diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 4e0722c25..a323d4095 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -28,7 +28,10 @@ import { useAlert } from 'dashboard/composables'; import { vOnClickOutside } from '@vueuse/components'; import { BUS_EVENTS } from 'shared/constants/busEvents'; -import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; +import { + CONVERSATION_EVENTS, + CAPTAIN_EVENTS, +} from 'dashboard/helper/AnalyticsHelper/events'; import { MESSAGE_EDITOR_IMAGE_RESIZES } from 'dashboard/constants/editor'; import { @@ -86,6 +89,7 @@ const props = defineProps({ // are triggered except when this flag is true allowSignature: { type: Boolean, default: false }, channelType: { type: String, default: '' }, + conversationId: { type: Number, default: null }, medium: { type: String, default: '' }, showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar focusOnMount: { type: Boolean, default: true }, @@ -396,7 +400,14 @@ function openFileBrowser() { } function handleCopilotClick() { - showSelectionMenu.value = !showSelectionMenu.value; + const isOpening = !showSelectionMenu.value; + if (isOpening) { + useTrack(CAPTAIN_EVENTS.EDITOR_AI_MENU_OPENED, { + conversationId: props.conversationId, + entryPoint: 'inline', + }); + } + showSelectionMenu.value = isOpening; } function handleClickOutside(event) { diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue index 0912cc698..09939f5d2 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue @@ -2,8 +2,10 @@ import { ref } from 'vue'; import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import { useCaptain } from 'dashboard/composables/useCaptain'; +import { useTrack } from 'dashboard/composables'; import { vOnClickOutside } from '@vueuse/components'; import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants'; +import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import NextButton from 'dashboard/components-next/button/Button.vue'; import EditorModeToggle from './EditorModeToggle.vue'; import CopilotMenuBar from './CopilotMenuBar.vue'; @@ -31,6 +33,10 @@ export default { type: Boolean, default: false, }, + conversationId: { + type: Number, + default: null, + }, isMessageLengthReachingThreshold: { type: Boolean, default: () => false, @@ -69,7 +75,14 @@ export default { }; const toggleCopilotMenu = () => { - showCopilotMenu.value = !showCopilotMenu.value; + const isOpening = !showCopilotMenu.value; + if (isOpening) { + useTrack(CAPTAIN_EVENTS.EDITOR_AI_MENU_OPENED, { + conversationId: props.conversationId, + entryPoint: 'top_panel', + }); + } + showCopilotMenu.value = isOpening; }; const handleClickOutside = () => { diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index e9d2f6d08..ee63aa711 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -41,7 +41,10 @@ import { truncatePreviewText, appendQuotedTextToMessage, } from 'dashboard/helper/quotedEmailHelper'; -import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; +import { + CONVERSATION_EVENTS, + CAPTAIN_EVENTS, +} from '../../../helper/AnalyticsHelper/events'; import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; import { appendSignature, @@ -136,6 +139,7 @@ export default { newConversationModalActive: false, showArticleSearchPopover: false, hasRecordedAudio: false, + copilotAcceptedMessages: {}, }; }, computed: { @@ -508,6 +512,24 @@ export default { emitter.off(CMD_AI_ASSIST, this.executeCopilotAction); }, methods: { + getDraftKey( + conversationId = this.conversationIdByRoute, + replyType = this.replyType + ) { + return `draft-${conversationId}-${replyType}`; + }, + getCopilotAcceptedMessage(replyType = this.replyType) { + const key = this.getDraftKey(this.conversationIdByRoute, replyType); + return this.copilotAcceptedMessages[key] || ''; + }, + setCopilotAcceptedMessage(message, replyType = this.replyType) { + const key = this.getDraftKey(this.conversationIdByRoute, replyType); + this.copilotAcceptedMessages[key] = trimContent(message || ''); + }, + clearCopilotAcceptedMessage(replyType = this.replyType) { + const key = this.getDraftKey(this.conversationIdByRoute, replyType); + delete this.copilotAcceptedMessages[key]; + }, handleInsert(article) { const { url, title } = article; // Removing empty lines from the title @@ -559,7 +581,7 @@ export default { }, saveDraft(conversationId, replyType) { if (this.message || this.message === '') { - const key = `draft-${conversationId}-${replyType}`; + const key = this.getDraftKey(conversationId, replyType); const draftToSave = trimContent(this.message || ''); this.$store.dispatch('draftMessages/set', { @@ -574,7 +596,7 @@ export default { }, getFromDraft() { if (this.conversationIdByRoute) { - const key = `draft-${this.conversationIdByRoute}-${this.replyType}`; + const key = this.getDraftKey(); const messageFromStore = this.$store.getters['draftMessages/get'](key) || ''; @@ -597,7 +619,7 @@ export default { }, removeFromDraft() { if (this.conversationIdByRoute) { - const key = `draft-${this.conversationIdByRoute}-${this.replyType}`; + const key = this.getDraftKey(); this.$store.dispatch('draftMessages/delete', { key }); } }, @@ -708,6 +730,7 @@ export default { return; } if (!this.showMentions) { + const copilotAcceptedMessage = this.getCopilotAcceptedMessage(); const isOnWhatsApp = this.isATwilioWhatsAppChannel || this.isAWhatsAppCloudChannel || @@ -717,10 +740,17 @@ export default { // This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages. const isOnInstagram = this.isAnInstagramChannel; if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) { - this.sendMessageAsMultipleMessages(this.message); + this.sendMessageAsMultipleMessages( + this.message, + copilotAcceptedMessage + ); } else { const messagePayload = this.getMessagePayload(this.message); - this.sendMessage(messagePayload); + this.sendMessage( + messagePayload, + this.message, + copilotAcceptedMessage + ); } if (!this.isPrivate) { @@ -732,13 +762,53 @@ export default { this.$emit('update:popOutReplyBox', false); } }, - sendMessageAsMultipleMessages(message) { + sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') { const messages = this.getMultipleMessagesPayload(message); messages.forEach(messagePayload => { - this.sendMessage(messagePayload); + this.sendMessage( + messagePayload, + messagePayload.message || '', + copilotAcceptedMessage + ); }); }, - sendMessageAnalyticsData(isPrivate) { + sendMessageAnalyticsData( + isPrivate, + { editorMessage = '', copilotAcceptedMessage = '' } = {} + ) { + const normalizeForComparison = message => { + let normalizedMessage = message || ''; + + if (this.sendWithSignature && this.messageSignature && !isPrivate) { + const effectiveChannelType = getEffectiveChannelType( + this.channelType, + this.inbox?.medium || '' + ); + normalizedMessage = removeSignature( + normalizedMessage, + this.messageSignature, + effectiveChannelType + ); + } + + return trimContent(normalizedMessage); + }; + + const normalizedAcceptedMessage = normalizeForComparison( + copilotAcceptedMessage + ); + const normalizedEditorMessage = normalizeForComparison(editorMessage); + + if (normalizedAcceptedMessage && normalizedEditorMessage) { + useTrack(CAPTAIN_EVENTS.AI_ASSISTED_MESSAGE_SENT, { + conversationId: this.conversationIdByRoute, + channelType: this.channelType, + editedBeforeSend: + normalizedAcceptedMessage !== normalizedEditorMessage, + isPrivate, + }); + } + // Analytics data for message signature is enabled or not in channels return isPrivate ? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE) @@ -772,7 +842,11 @@ export default { this.confirmOnSendReply(); } }, - async sendMessage(messagePayload) { + async sendMessage( + messagePayload, + editorMessage = '', + copilotAcceptedMessage = '' + ) { try { await this.$store.dispatch( 'createPendingMessageAndSend', @@ -781,7 +855,10 @@ export default { emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE); emitter.emit(BUS_EVENTS.MESSAGE_SENT); this.removeFromDraft(); - this.sendMessageAnalyticsData(messagePayload.private); + this.sendMessageAnalyticsData(messagePayload.private, { + editorMessage, + copilotAcceptedMessage, + }); } catch (error) { const errorMessage = error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR'); @@ -855,6 +932,7 @@ export default { }, clearMessage() { this.message = ''; + this.clearCopilotAcceptedMessage(); if (this.sendWithSignature && !this.isPrivate) { // if signature is enabled, append it to the message const effectiveChannelType = getEffectiveChannelType( @@ -1119,7 +1197,9 @@ export default { this.$emit('update:popOutReplyBox', !this.popOutReplyBox); }, onSubmitCopilotReply() { - this.message = this.copilot.accept(); + const acceptedMessage = this.copilot.accept(); + this.message = acceptedMessage; + this.setCopilotAcceptedMessage(acceptedMessage); }, }, }; @@ -1130,6 +1210,7 @@ export default {