## Description
Adds missing analytics instrumentation for the editor AI funnel so we
can measure end-to-end usage and outcome quality.
### What was added
- Captain: Editor AI menu opened
- Captain: Generation failed
- Captain: AI-assisted message sent
### Behavior covered
- Tracks AI button click + menu open from both entry points:
- top panel sparkle button
- inline editor copilot button
- Tracks generation failures (initial + follow-up stages).
- Tracks whether accepted AI content was sent as-is or edited before
send.
### Notes
- Applies to editor Captain accept/send flow
(rewrite/summarize/reply_suggestion + follow-ups).
- Does not change Copilot sidebar flow instrumentation.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
### Manual verification steps
<img width="1906" height="832" alt="image"
src="https://github.com/user-attachments/assets/f0ade43b-aa8d-41be-8ca2-20a091a81f60"
/>
<img width="828" height="280" alt="image"
src="https://github.com/user-attachments/assets/be76219e-fb61-4a6e-bff5-dc085b0a3cc9"
/>
<img width="415" height="147" alt="image"
src="https://github.com/user-attachments/assets/36802c5c-33a7-49ed-bf7e-f0b02d86dccc"
/>
<img width="2040" height="516" alt="image"
src="https://github.com/user-attachments/assets/74b95288-bc86-4312-a282-14211ae8f25c"
/>
1. Open a conversation with Captain tasks enabled.
2. Click AI button in top panel and inline editor.
3. Confirm analytics events fire for:
- AI menu opened
4. Run an AI action and force a failure scenario (or empty response
path) and confirm generation-failed event.
5. Accept AI output, then:
- send without changes -> editedBeforeSend: false
- edit then send -> editedBeforeSend: true
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
247 lines
8.0 KiB
JavaScript
247 lines
8.0 KiB
JavaScript
import { computed } from 'vue';
|
|
import {
|
|
useFunctionGetter,
|
|
useMapGetter,
|
|
useStore,
|
|
} from 'dashboard/composables/store.js';
|
|
import { useAccount } from 'dashboard/composables/useAccount';
|
|
import { useConfig } from 'dashboard/composables/useConfig';
|
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
|
import { useAlert } from 'dashboard/composables';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
import TasksAPI from 'dashboard/api/captain/tasks';
|
|
import { CAPTAIN_ERROR_TYPES } from 'dashboard/composables/captain/constants';
|
|
|
|
export function useCaptain() {
|
|
const store = useStore();
|
|
const { t } = useI18n();
|
|
const { isCloudFeatureEnabled, currentAccount } = useAccount();
|
|
const { isEnterprise } = useConfig();
|
|
const uiFlags = useMapGetter('accounts/getUIFlags');
|
|
const currentChat = useMapGetter('getSelectedChat');
|
|
const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
|
|
const conversationId = computed(() => currentChat.value?.id);
|
|
const draftKey = computed(
|
|
() => `draft-${conversationId.value}-${replyMode.value}`
|
|
);
|
|
const draftMessage = useFunctionGetter('draftMessages/get', draftKey);
|
|
|
|
// === Feature Flags ===
|
|
const captainEnabled = computed(() => {
|
|
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN);
|
|
});
|
|
|
|
const captainTasksEnabled = computed(() => {
|
|
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
|
|
});
|
|
|
|
// === Limits (Enterprise) ===
|
|
const captainLimits = computed(() => {
|
|
return currentAccount.value?.limits?.captain;
|
|
});
|
|
|
|
const documentLimits = computed(() => {
|
|
if (captainLimits.value?.documents) {
|
|
return useCamelCase(captainLimits.value.documents);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const responseLimits = computed(() => {
|
|
if (captainLimits.value?.responses) {
|
|
return useCamelCase(captainLimits.value.responses);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const isFetchingLimits = computed(() => uiFlags.value.isFetchingLimits);
|
|
|
|
const fetchLimits = () => {
|
|
if (isEnterprise) {
|
|
store.dispatch('accounts/limits');
|
|
}
|
|
};
|
|
|
|
// === Error Handling ===
|
|
/**
|
|
* Handles API errors and displays appropriate error messages.
|
|
* Silently returns for aborted requests.
|
|
* @param {Error} error - The error object from the API call.
|
|
*/
|
|
const handleAPIError = error => {
|
|
if (
|
|
error.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
|
error.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
|
) {
|
|
return;
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR');
|
|
useAlert(errorMessage);
|
|
};
|
|
|
|
/**
|
|
* Classifies API error types for downstream analytics.
|
|
* @param {Error} error
|
|
* @returns {string}
|
|
*/
|
|
const getErrorType = error => {
|
|
if (
|
|
error.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
|
error.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
|
) {
|
|
return CAPTAIN_ERROR_TYPES.ABORTED;
|
|
}
|
|
if (error.response?.status) {
|
|
return `${CAPTAIN_ERROR_TYPES.HTTP_PREFIX}${error.response.status}`;
|
|
}
|
|
return CAPTAIN_ERROR_TYPES.API_ERROR;
|
|
};
|
|
|
|
// === Task Methods ===
|
|
/**
|
|
* Rewrites content with a specific operation.
|
|
* @param {string} content - The content to rewrite.
|
|
* @param {string} operation - The operation (fix_spelling_grammar, casual, professional, expand, shorten, improve, etc).
|
|
* @param {Object} [options={}] - Additional options.
|
|
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
|
* @returns {Promise<{message: string, followUpContext?: Object}>} The rewritten content and optional follow-up context.
|
|
*/
|
|
const rewriteContent = async (content, operation, options = {}) => {
|
|
try {
|
|
const result = await TasksAPI.rewrite(
|
|
{
|
|
content: content || draftMessage.value,
|
|
operation,
|
|
conversationId: conversationId.value,
|
|
},
|
|
options.signal
|
|
);
|
|
const {
|
|
data: { message: generatedMessage, follow_up_context: followUpContext },
|
|
} = result;
|
|
return { message: generatedMessage, followUpContext };
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
return { message: '', errorType: getErrorType(error) };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Summarizes a conversation.
|
|
* @param {Object} [options={}] - Additional options.
|
|
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
|
* @returns {Promise<{message: string, followUpContext?: Object}>} The summary and optional follow-up context.
|
|
*/
|
|
const summarizeConversation = async (options = {}) => {
|
|
try {
|
|
const result = await TasksAPI.summarize(
|
|
conversationId.value,
|
|
options.signal
|
|
);
|
|
const {
|
|
data: { message: generatedMessage, follow_up_context: followUpContext },
|
|
} = result;
|
|
return { message: generatedMessage, followUpContext };
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
return { message: '', errorType: getErrorType(error) };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets a reply suggestion for the current conversation.
|
|
* @param {Object} [options={}] - Additional options.
|
|
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
|
* @returns {Promise<{message: string, followUpContext?: Object}>} The reply suggestion and optional follow-up context.
|
|
*/
|
|
const getReplySuggestion = async (options = {}) => {
|
|
try {
|
|
const result = await TasksAPI.replySuggestion(
|
|
conversationId.value,
|
|
options.signal
|
|
);
|
|
const {
|
|
data: { message: generatedMessage, follow_up_context: followUpContext },
|
|
} = result;
|
|
return { message: generatedMessage, followUpContext };
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
return { message: '', errorType: getErrorType(error) };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sends a follow-up message to refine a previous AI task result.
|
|
* @param {Object} options - The follow-up options.
|
|
* @param {Object} options.followUpContext - The follow-up context from a previous task.
|
|
* @param {string} options.message - The follow-up message/request from the user.
|
|
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
|
* @returns {Promise<{message: string, followUpContext: Object}>} The follow-up response and updated context.
|
|
*/
|
|
const followUp = async ({ followUpContext, message, signal }) => {
|
|
try {
|
|
const result = await TasksAPI.followUp(
|
|
{ followUpContext, message, conversationId: conversationId.value },
|
|
signal
|
|
);
|
|
const {
|
|
data: { message: generatedMessage, follow_up_context: updatedContext },
|
|
} = result;
|
|
return { message: generatedMessage, followUpContext: updatedContext };
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
return {
|
|
message: '',
|
|
followUpContext,
|
|
errorType: getErrorType(error),
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Processes an AI event. Routes to the appropriate method based on type.
|
|
* @param {string} [type='improve'] - The type of AI event to process.
|
|
* @param {string} [content=''] - The content to process.
|
|
* @param {Object} [options={}] - Additional options.
|
|
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
|
* @returns {Promise<{message: string, followUpContext?: Object}>} The generated message and optional follow-up context.
|
|
*/
|
|
const processEvent = async (type = 'improve', content = '', options = {}) => {
|
|
if (type === 'summarize') {
|
|
return summarizeConversation(options);
|
|
}
|
|
if (type === 'reply_suggestion') {
|
|
return getReplySuggestion(options);
|
|
}
|
|
// All other types are rewrite operations
|
|
return rewriteContent(content, type, options);
|
|
};
|
|
|
|
return {
|
|
// Feature flags
|
|
captainEnabled,
|
|
captainTasksEnabled,
|
|
|
|
// Limits (Enterprise)
|
|
captainLimits,
|
|
documentLimits,
|
|
responseLimits,
|
|
fetchLimits,
|
|
isFetchingLimits,
|
|
|
|
// Conversation context
|
|
draftMessage,
|
|
currentChat,
|
|
|
|
// Task methods
|
|
rewriteContent,
|
|
summarizeConversation,
|
|
getReplySuggestion,
|
|
followUp,
|
|
processEvent,
|
|
};
|
|
}
|