374 lines
10 KiB
JavaScript
374 lines
10 KiB
JavaScript
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';
|
|
import {
|
|
CAPTAIN_ERROR_TYPES,
|
|
CAPTAIN_GENERATION_FAILURE_REASONS,
|
|
} from 'dashboard/composables/captain/constants';
|
|
|
|
// 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;
|
|
}
|
|
|
|
function trackGenerationFailure({
|
|
action,
|
|
conversationId,
|
|
followUpCount = undefined,
|
|
stage,
|
|
reason,
|
|
}) {
|
|
useTrack(CAPTAIN_EVENTS.GENERATION_FAILED, {
|
|
...buildPayload(action, conversationId, followUpCount),
|
|
stage,
|
|
reason,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Composable for managing Copilot reply generation state and actions.
|
|
* Extracts copilot-related logic from ReplyBox for cleaner code organization.
|
|
*
|
|
* @returns {Object} Copilot reply state and methods
|
|
*/
|
|
export function useCopilotReply() {
|
|
const { processEvent, followUp, currentChat } = useCaptain();
|
|
const { updateUISettings } = useUISettings();
|
|
|
|
const showEditor = ref(false);
|
|
const isGenerating = ref(false);
|
|
const isContentReady = ref(false);
|
|
const generatedContent = ref('');
|
|
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
|
|
);
|
|
const editorTransitionKey = computed(() =>
|
|
isActive.value ? 'copilot' : 'rich'
|
|
);
|
|
|
|
/**
|
|
* Resets all copilot editor state and cancels any ongoing generation.
|
|
* @param {boolean} [trackDismiss=true] - Whether to track dismiss event
|
|
*/
|
|
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;
|
|
}
|
|
showEditor.value = false;
|
|
isGenerating.value = false;
|
|
isContentReady.value = false;
|
|
generatedContent.value = '';
|
|
followUpContext.value = null;
|
|
currentAction.value = null;
|
|
followUpCount.value = 0;
|
|
trackedConversationId.value = null;
|
|
}
|
|
|
|
/**
|
|
* Toggles the copilot editor visibility.
|
|
*/
|
|
function toggleEditor() {
|
|
showEditor.value = !showEditor.value;
|
|
}
|
|
|
|
/**
|
|
* Marks content as ready (called after transition completes).
|
|
*/
|
|
function setContentReady() {
|
|
isContentReady.value = true;
|
|
}
|
|
|
|
/**
|
|
* Executes a copilot action (e.g., improve, fix grammar).
|
|
* @param {string} action - The action type
|
|
* @param {string} data - The content to process
|
|
*/
|
|
async function execute(action, data) {
|
|
if (action === 'ask_copilot') {
|
|
updateUISettings({
|
|
is_contact_sidebar_open: false,
|
|
is_copilot_panel_open: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Reset without tracking dismiss (starting new action)
|
|
reset(false);
|
|
const requestController = new AbortController();
|
|
abortController.value = requestController;
|
|
isGenerating.value = true;
|
|
isContentReady.value = false;
|
|
currentAction.value = action;
|
|
followUpCount.value = 0;
|
|
trackedConversationId.value = conversationId.value;
|
|
|
|
try {
|
|
const {
|
|
message: content,
|
|
followUpContext: newContext,
|
|
errorType,
|
|
} = await processEvent(action, data, {
|
|
signal: requestController.signal,
|
|
});
|
|
|
|
if (requestController.signal.aborted) return;
|
|
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
|
|
if (abortController.value === requestController) {
|
|
isGenerating.value = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
generatedContent.value = content;
|
|
followUpContext.value = newContext;
|
|
if (content) {
|
|
showEditor.value = true;
|
|
// Track "Used" event on successful generation
|
|
const eventKey = `${getEventPrefix(action)}_USED`;
|
|
useTrack(
|
|
CAPTAIN_EVENTS[eventKey],
|
|
buildPayload(action, trackedConversationId.value)
|
|
);
|
|
} else if (errorType && errorType !== CAPTAIN_ERROR_TYPES.ABORTED) {
|
|
trackGenerationFailure({
|
|
action,
|
|
conversationId: trackedConversationId.value,
|
|
stage: 'initial',
|
|
reason: errorType,
|
|
});
|
|
} else {
|
|
trackGenerationFailure({
|
|
action,
|
|
conversationId: trackedConversationId.value,
|
|
stage: 'initial',
|
|
reason: CAPTAIN_GENERATION_FAILURE_REASONS.EMPTY_RESPONSE,
|
|
});
|
|
}
|
|
isGenerating.value = false;
|
|
} catch (error) {
|
|
if (
|
|
requestController.signal.aborted ||
|
|
error?.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
|
error?.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
|
) {
|
|
return;
|
|
}
|
|
trackGenerationFailure({
|
|
action,
|
|
conversationId: trackedConversationId.value,
|
|
stage: 'initial',
|
|
reason: error?.name || CAPTAIN_GENERATION_FAILURE_REASONS.EXCEPTION,
|
|
});
|
|
isGenerating.value = false;
|
|
} finally {
|
|
if (abortController.value === requestController) {
|
|
abortController.value = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a follow-up message to refine the current generated content.
|
|
* @param {string} message - The follow-up message from the user
|
|
*/
|
|
async function sendFollowUp(message) {
|
|
if (!followUpContext.value || !message.trim()) return;
|
|
|
|
const requestController = new AbortController();
|
|
abortController.value = requestController;
|
|
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,
|
|
errorType,
|
|
} = await followUp({
|
|
followUpContext: followUpContext.value,
|
|
message,
|
|
signal: requestController.signal,
|
|
});
|
|
|
|
if (requestController.signal.aborted) return;
|
|
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
|
|
if (abortController.value === requestController) {
|
|
isGenerating.value = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (content) {
|
|
generatedContent.value = content;
|
|
followUpContext.value = updatedContext;
|
|
showEditor.value = true;
|
|
} else if (errorType && errorType !== CAPTAIN_ERROR_TYPES.ABORTED) {
|
|
trackGenerationFailure({
|
|
action: currentAction.value,
|
|
conversationId: trackedConversationId.value,
|
|
followUpCount: followUpCount.value,
|
|
stage: 'follow_up',
|
|
reason: errorType,
|
|
});
|
|
} else {
|
|
trackGenerationFailure({
|
|
action: currentAction.value,
|
|
conversationId: trackedConversationId.value,
|
|
followUpCount: followUpCount.value,
|
|
stage: 'follow_up',
|
|
reason: CAPTAIN_GENERATION_FAILURE_REASONS.EMPTY_RESPONSE,
|
|
});
|
|
}
|
|
isGenerating.value = false;
|
|
} catch (error) {
|
|
if (
|
|
requestController.signal.aborted ||
|
|
error?.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
|
error?.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
|
) {
|
|
return;
|
|
}
|
|
trackGenerationFailure({
|
|
action: currentAction.value,
|
|
conversationId: trackedConversationId.value,
|
|
followUpCount: followUpCount.value,
|
|
stage: 'follow_up',
|
|
reason: error?.name || CAPTAIN_GENERATION_FAILURE_REASONS.EXCEPTION,
|
|
});
|
|
isGenerating.value = false;
|
|
} finally {
|
|
if (abortController.value === requestController) {
|
|
abortController.value = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accepts the generated content and returns it.
|
|
* Note: Formatting is automatically stripped by the Editor component's
|
|
* createState function based on the channel's schema.
|
|
* @returns {string} The content ready for the editor
|
|
*/
|
|
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;
|
|
}
|
|
|
|
return {
|
|
showEditor,
|
|
isGenerating,
|
|
isContentReady,
|
|
generatedContent,
|
|
followUpContext,
|
|
|
|
isActive,
|
|
isButtonDisabled,
|
|
editorTransitionKey,
|
|
|
|
reset,
|
|
toggleEditor,
|
|
setContentReady,
|
|
execute,
|
|
sendFollowUp,
|
|
accept,
|
|
};
|
|
}
|