mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Display banner and handoff for bot-managed chats (#12292)
This commit is contained in:
@@ -15,7 +15,7 @@ import ReplyEmailHead from './ReplyEmailHead.vue';
|
|||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||||
@@ -53,8 +53,8 @@ export default {
|
|||||||
ArticleSearchPopover,
|
ArticleSearchPopover,
|
||||||
AttachmentPreview,
|
AttachmentPreview,
|
||||||
AudioRecorder,
|
AudioRecorder,
|
||||||
Banner,
|
|
||||||
CannedResponse,
|
CannedResponse,
|
||||||
|
ReplyBoxBanner,
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
MessageSignatureMissingAlert,
|
MessageSignatureMissingAlert,
|
||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
@@ -158,35 +158,6 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
assignedAgent: {
|
|
||||||
get() {
|
|
||||||
return this.currentChat.meta.assignee;
|
|
||||||
},
|
|
||||||
set(agent) {
|
|
||||||
const agentId = agent ? agent.id : 0;
|
|
||||||
this.$store.dispatch('setCurrentChatAssignee', agent);
|
|
||||||
this.$store
|
|
||||||
.dispatch('assignAgent', {
|
|
||||||
conversationId: this.currentChat.id,
|
|
||||||
agentId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
showSelfAssignBanner() {
|
|
||||||
if (this.message !== '' && !this.isOnPrivateNote) {
|
|
||||||
if (!this.assignedAgent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.assignedAgent.id !== this.currentUser.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
showWhatsappTemplates() {
|
showWhatsappTemplates() {
|
||||||
return this.isAWhatsAppCloudChannel && !this.isPrivate;
|
return this.isAWhatsAppCloudChannel && !this.isPrivate;
|
||||||
},
|
},
|
||||||
@@ -671,29 +642,6 @@ export default {
|
|||||||
hideContentTemplatesModal() {
|
hideContentTemplatesModal() {
|
||||||
this.showContentTemplatesModal = false;
|
this.showContentTemplatesModal = false;
|
||||||
},
|
},
|
||||||
onClickSelfAssign() {
|
|
||||||
const {
|
|
||||||
account_id,
|
|
||||||
availability_status,
|
|
||||||
available_name,
|
|
||||||
email,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
role,
|
|
||||||
avatar_url,
|
|
||||||
} = this.currentUser;
|
|
||||||
const selfAssign = {
|
|
||||||
account_id,
|
|
||||||
availability_status,
|
|
||||||
available_name,
|
|
||||||
email,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
role,
|
|
||||||
thumbnail: avatar_url,
|
|
||||||
};
|
|
||||||
this.assignedAgent = selfAssign;
|
|
||||||
},
|
|
||||||
confirmOnSendReply() {
|
confirmOnSendReply() {
|
||||||
if (this.isReplyButtonDisabled) {
|
if (this.isReplyButtonDisabled) {
|
||||||
return;
|
return;
|
||||||
@@ -1118,16 +1066,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Banner
|
<ReplyBoxBanner :message="message" :is-on-private-note="isOnPrivateNote" />
|
||||||
v-if="showSelfAssignBanner"
|
|
||||||
action-button-variant="ghost"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class="mx-2 mb-2 rounded-lg banner--self-assign"
|
|
||||||
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
|
||||||
has-action-button
|
|
||||||
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
|
||||||
@primary-action="onClickSelfAssign"
|
|
||||||
/>
|
|
||||||
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
||||||
<ReplyTopPanel
|
<ReplyTopPanel
|
||||||
:mode="replyType"
|
:mode="replyType"
|
||||||
@@ -1293,10 +1232,6 @@ export default {
|
|||||||
@apply mb-0;
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner--self-assign {
|
|
||||||
@apply py-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-preview-box {
|
.attachment-preview-box {
|
||||||
@apply bg-transparent py-0 px-4;
|
@apply bg-transparent py-0 px-4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
|
||||||
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isOnPrivateNote: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
|
|
||||||
|
const assignedAgent = computed({
|
||||||
|
get() {
|
||||||
|
return currentChat.value?.meta?.assignee;
|
||||||
|
},
|
||||||
|
set(agent) {
|
||||||
|
const agentId = agent ? agent.id : 0;
|
||||||
|
store.dispatch('setCurrentChatAssignee', agent);
|
||||||
|
store.dispatch('assignAgent', {
|
||||||
|
conversationId: currentChat.value?.id,
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isUserTyping = computed(
|
||||||
|
() => props.message !== '' && !props.isOnPrivateNote
|
||||||
|
);
|
||||||
|
const isUnassigned = computed(() => !assignedAgent.value);
|
||||||
|
const isAssignedToOtherAgent = computed(
|
||||||
|
() => assignedAgent.value?.id !== currentUser.value?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const showSelfAssignBanner = computed(() => {
|
||||||
|
return (
|
||||||
|
isUserTyping.value && (isUnassigned.value || isAssignedToOtherAgent.value)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showBotHandoffBanner = computed(
|
||||||
|
() =>
|
||||||
|
isUserTyping.value &&
|
||||||
|
currentChat.value?.status === wootConstants.STATUS_TYPE.PENDING
|
||||||
|
);
|
||||||
|
|
||||||
|
const botHandoffActionLabel = computed(() => {
|
||||||
|
return assignedAgent.value?.id === currentUser.value?.id
|
||||||
|
? t('CONVERSATION.BOT_HANDOFF_REOPEN_ACTION')
|
||||||
|
: t('CONVERSATION.BOT_HANDOFF_ACTION');
|
||||||
|
});
|
||||||
|
|
||||||
|
const selfAssignConversation = async () => {
|
||||||
|
const { avatar_url, ...rest } = currentUser.value || {};
|
||||||
|
assignedAgent.value = { ...rest, thumbnail: avatar_url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const needsAssignmentToCurrentUser = computed(() => {
|
||||||
|
return isUnassigned.value || isAssignedToOtherAgent.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClickSelfAssign = async () => {
|
||||||
|
try {
|
||||||
|
await selfAssignConversation();
|
||||||
|
useAlert(t('CONVERSATION.CHANGE_AGENT'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('CONVERSATION.CHANGE_AGENT_FAILED'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reopenConversation = async () => {
|
||||||
|
await store.dispatch('toggleStatus', {
|
||||||
|
conversationId: currentChat.value?.id,
|
||||||
|
status: wootConstants.STATUS_TYPE.OPEN,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickBotHandoff = async () => {
|
||||||
|
try {
|
||||||
|
await reopenConversation();
|
||||||
|
|
||||||
|
if (needsAssignmentToCurrentUser.value) {
|
||||||
|
await selfAssignConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
useAlert(t('CONVERSATION.BOT_HANDOFF_SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('CONVERSATION.BOT_HANDOFF_ERROR'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Banner
|
||||||
|
v-if="showSelfAssignBanner && !showBotHandoffBanner"
|
||||||
|
action-button-variant="ghost"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class="mx-2 mb-2 rounded-lg !py-2"
|
||||||
|
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
||||||
|
has-action-button
|
||||||
|
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
||||||
|
@primary-action="onClickSelfAssign"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="showBotHandoffBanner"
|
||||||
|
action-button-variant="ghost"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class="mx-2 mb-2 rounded-lg !py-2"
|
||||||
|
:banner-message="$t('CONVERSATION.BOT_HANDOFF_MESSAGE')"
|
||||||
|
has-action-button
|
||||||
|
:action-button-label="botHandoffActionLabel"
|
||||||
|
@primary-action="onClickBotHandoff"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||||
"ASSIGN_TO_ME": "Assign to me",
|
"ASSIGN_TO_ME": "Assign to me",
|
||||||
|
"BOT_HANDOFF_MESSAGE": "You are responding to a conversation which is currently handled by an assistant or a bot.",
|
||||||
|
"BOT_HANDOFF_ACTION": "Mark open and assign to you",
|
||||||
|
"BOT_HANDOFF_REOPEN_ACTION": "Mark conversation open",
|
||||||
|
"BOT_HANDOFF_SUCCESS": "Conversation has been handed over to you",
|
||||||
|
"BOT_HANDOFF_ERROR": "Failed to take over the conversation. Please try again.",
|
||||||
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
||||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
||||||
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You won’t be able to send messages from this conversation anymore.",
|
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You won’t be able to send messages from this conversation anymore.",
|
||||||
|
|||||||
Reference in New Issue
Block a user