diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 7467c2fa8..e9d2f6d08 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -1271,7 +1271,7 @@ export default { key="copilot-bottom-panel" :is-generating-content="copilot.isButtonDisabled.value" @submit="onSubmitCopilotReply" - @cancel="copilot.toggleEditor" + @cancel="copilot.reset" /> { return actual; }); vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({ - OPEN_AI_EVENTS: { - TEST_EVENT: 'open_ai_test_event', + CAPTAIN_EVENTS: { + TEST_EVENT: 'captain_test_event', }, })); @@ -65,17 +64,6 @@ describe('useCaptain', () => { expect(draftMessage.value).toBe('Draft message'); }); - it('records analytics correctly', async () => { - const { recordAnalytics } = useCaptain(); - - await recordAnalytics('TEST_EVENT', { data: 'test' }); - - expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', { - type: 'TEST_EVENT', - data: 'test', - }); - }); - it('rewrites content', async () => { TasksAPI.rewrite.mockResolvedValue({ data: { message: 'Rewritten content', follow_up_context: { id: 'ctx1' } }, diff --git a/app/javascript/dashboard/composables/useCaptain.js b/app/javascript/dashboard/composables/useCaptain.js index 507f35864..f7c72e04f 100644 --- a/app/javascript/dashboard/composables/useCaptain.js +++ b/app/javascript/dashboard/composables/useCaptain.js @@ -7,10 +7,9 @@ import { import { useAccount } from 'dashboard/composables/useAccount'; import { useConfig } from 'dashboard/composables/useConfig'; import { useCamelCase } from 'dashboard/composables/useTransformKeys'; -import { useAlert, useTrack } from 'dashboard/composables'; +import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; -import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import TasksAPI from 'dashboard/api/captain/tasks'; export function useCaptain() { @@ -79,23 +78,6 @@ export function useCaptain() { useAlert(errorMessage); }; - // === Analytics === - /** - * Records analytics for AI-related events. - * @param {string} type - The type of event. - * @param {Object} payload - Additional data for the event. - * @returns {Promise} - */ - const recordAnalytics = async (type, payload) => { - const event = OPEN_AI_EVENTS[type.toUpperCase()]; - if (event) { - useTrack(event, { - type, - ...payload, - }); - } - }; - // === Task Methods === /** * Rewrites content with a specific operation. @@ -234,8 +216,5 @@ export function useCaptain() { getReplySuggestion, followUp, processEvent, - - // Analytics - recordAnalytics, }; } diff --git a/app/javascript/dashboard/composables/useCopilotReply.js b/app/javascript/dashboard/composables/useCopilotReply.js index 83b541700..492bcb43e 100644 --- a/app/javascript/dashboard/composables/useCopilotReply.js +++ b/app/javascript/dashboard/composables/useCopilotReply.js @@ -1,6 +1,56 @@ import { ref, computed } from 'vue'; import { useCaptain } from 'dashboard/composables/useCaptain'; import { useUISettings } from 'dashboard/composables/useUISettings'; +import { useTrack } from 'dashboard/composables'; +import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; + +// Actions that map to REWRITE events (with operation attribute) +const REWRITE_ACTIONS = [ + 'improve', + 'fix_spelling_grammar', + 'casual', + 'professional', + 'expand', + 'shorten', + 'rephrase', + 'make_friendly', + 'make_formal', + 'simplify', +]; + +/** + * Gets the event key suffix based on action type. + * @param {string} action - The action type + * @returns {string} The event key prefix (REWRITE, SUMMARIZE, or REPLY_SUGGESTION) + */ +function getEventPrefix(action) { + if (action === 'summarize') return 'SUMMARIZE'; + if (action === 'reply_suggestion') return 'REPLY_SUGGESTION'; + return 'REWRITE'; +} + +/** + * Builds the analytics payload based on action type. + * @param {string} action - The action type + * @param {number} conversationId - The conversation ID + * @param {number} [followUpCount] - Optional follow-up count + * @returns {Object} The payload object + */ +function buildPayload(action, conversationId, followUpCount = undefined) { + const payload = { conversationId }; + + // Add operation for rewrite actions + if (REWRITE_ACTIONS.includes(action)) { + payload.operation = action; + } + + // Add followUpCount if provided + if (followUpCount !== undefined) { + payload.followUpCount = followUpCount; + } + + return payload; +} /** * Composable for managing Copilot reply generation state and actions. @@ -9,7 +59,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings'; * @returns {Object} Copilot reply state and methods */ export function useCopilotReply() { - const { processEvent, followUp } = useCaptain(); + const { processEvent, followUp, currentChat } = useCaptain(); const { updateUISettings } = useUISettings(); const showEditor = ref(false); @@ -19,6 +69,13 @@ export function useCopilotReply() { const followUpContext = ref(null); const abortController = ref(null); + // Tracking state + const currentAction = ref(null); + const followUpCount = ref(0); + const trackedConversationId = ref(null); + + const conversationId = computed(() => currentChat.value?.id); + const isActive = computed(() => showEditor.value || isGenerating.value); const isButtonDisabled = computed( () => isGenerating.value || !isContentReady.value @@ -29,8 +86,22 @@ export function useCopilotReply() { /** * Resets all copilot editor state and cancels any ongoing generation. + * @param {boolean} [trackDismiss=true] - Whether to track dismiss event */ - function reset() { + function reset(trackDismiss = true) { + // Track dismiss event if there was content and we're not accepting + if (trackDismiss && generatedContent.value && currentAction.value) { + const eventKey = `${getEventPrefix(currentAction.value)}_DISMISSED`; + useTrack( + CAPTAIN_EVENTS[eventKey], + buildPayload( + currentAction.value, + trackedConversationId.value, + followUpCount.value + ) + ); + } + if (abortController.value) { abortController.value.abort(); abortController.value = null; @@ -40,6 +111,9 @@ export function useCopilotReply() { isContentReady.value = false; generatedContent.value = ''; followUpContext.value = null; + currentAction.value = null; + followUpCount.value = 0; + trackedConversationId.value = null; } /** @@ -70,11 +144,14 @@ export function useCopilotReply() { return; } - // Reset and start new generation - reset(); + // Reset without tracking dismiss (starting new action) + reset(false); abortController.value = new AbortController(); isGenerating.value = true; isContentReady.value = false; + currentAction.value = action; + followUpCount.value = 0; + trackedConversationId.value = conversationId.value; try { const { message: content, followUpContext: newContext } = @@ -85,7 +162,15 @@ export function useCopilotReply() { if (!abortController.value?.signal.aborted) { generatedContent.value = content; followUpContext.value = newContext; - if (content) showEditor.value = true; + if (content) { + showEditor.value = true; + // Track "Used" event on successful generation + const eventKey = `${getEventPrefix(action)}_USED`; + useTrack( + CAPTAIN_EVENTS[eventKey], + buildPayload(action, trackedConversationId.value) + ); + } isGenerating.value = false; } } catch { @@ -106,6 +191,12 @@ export function useCopilotReply() { isGenerating.value = true; isContentReady.value = false; + // Track follow-up sent event + useTrack(CAPTAIN_EVENTS.FOLLOW_UP_SENT, { + conversationId: trackedConversationId.value, + }); + followUpCount.value += 1; + try { const { message: content, followUpContext: updatedContext } = await followUp({ @@ -137,7 +228,28 @@ export function useCopilotReply() { */ function accept() { const content = generatedContent.value; + + // Track "Applied" event + if (currentAction.value) { + const eventKey = `${getEventPrefix(currentAction.value)}_APPLIED`; + useTrack( + CAPTAIN_EVENTS[eventKey], + buildPayload( + currentAction.value, + trackedConversationId.value, + followUpCount.value + ) + ); + } + + // Reset state without tracking dismiss showEditor.value = false; + generatedContent.value = ''; + followUpContext.value = null; + currentAction.value = null; + followUpCount.value = 0; + trackedConversationId.value = null; + return content; } diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index 8e28473d9..0b6e85d77 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -84,22 +84,28 @@ export const PORTALS_EVENTS = Object.freeze({ PREVIEW_ARTICLE: 'Previewed article', }); -export const OPEN_AI_EVENTS = Object.freeze({ - SUMMARIZE: 'OpenAI: Used summarize', - REPLY_SUGGESTION: 'OpenAI: Used reply suggestion', - REPHRASE: 'OpenAI: Used rephrase', - IMPROVE: 'OpenAI: Used improve', - FIX_SPELLING_AND_GRAMMAR: 'OpenAI: Used fix spelling and grammar', - SHORTEN: 'OpenAI: Used shorten', - EXPAND: 'OpenAI: Used expand', - MAKE_FRIENDLY: 'OpenAI: Used make friendly', - MAKE_FORMAL: 'OpenAI: Used make formal', - SIMPLIFY: 'OpenAI: Used simplify', - APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion', - DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions', - ADDED_AI_INTEGRATION_VIA_CTA_BUTTON: - 'OpenAI: Added AI integration via CTA button', - DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions', +export const CAPTAIN_EVENTS = Object.freeze({ + // Rewrite events (with operation attribute in payload) + REWRITE_USED: 'Captain: Rewrite used', + REWRITE_APPLIED: 'Captain: Rewrite applied', + REWRITE_DISMISSED: 'Captain: Rewrite dismissed', + + // Summarize events + SUMMARIZE_USED: 'Captain: Summarize used', + SUMMARIZE_APPLIED: 'Captain: Summarize applied', + SUMMARIZE_DISMISSED: 'Captain: Summarize dismissed', + + // Reply suggestion events + REPLY_SUGGESTION_USED: 'Captain: Reply suggestion used', + REPLY_SUGGESTION_APPLIED: 'Captain: Reply suggestion applied', + REPLY_SUGGESTION_DISMISSED: 'Captain: Reply suggestion dismissed', + + // Follow-up events + FOLLOW_UP_SENT: 'Captain: Follow-up sent', + + // Label suggestions + LABEL_SUGGESTION_APPLIED: 'Captain: Label suggestion applied', + LABEL_SUGGESTION_DISMISSED: 'Captain: Label suggestion dismissed', }); export const COPILOT_EVENTS = Object.freeze({