Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance

This commit is contained in:
Muhsin Keloth
2026-03-17 15:58:48 +04:00
committed by GitHub
2183 changed files with 60007 additions and 17719 deletions
@@ -0,0 +1,12 @@
export const CAPTAIN_ERROR_TYPES = Object.freeze({
ABORTED: 'aborted',
API_ERROR: 'api_error',
HTTP_PREFIX: 'http_',
ABORT_ERROR: 'AbortError',
CANCELED_ERROR: 'CanceledError',
});
export const CAPTAIN_GENERATION_FAILURE_REASONS = Object.freeze({
EMPTY_RESPONSE: 'empty_response',
EXCEPTION: 'exception',
});
@@ -102,6 +102,28 @@ export function useBulkActions() {
}
}
// Only used in context menu
async function onRemoveLabels(labelsToRemove, conversationId = null) {
try {
await store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: conversationId || selectedConversations.value,
labels: {
remove: labelsToRemove,
},
});
useAlert(
t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.SUCCESFUL', {
labelName: labelsToRemove[0],
conversationId,
})
);
} catch (err) {
useAlert(t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.FAILED'));
}
}
async function onAssignTeamsForBulk(team) {
try {
await store.dispatch('bulkActions/process', {
@@ -189,6 +211,7 @@ export function useBulkActions() {
isConversationSelected,
onAssignAgent,
onAssignLabels,
onRemoveLabels,
onAssignTeamsForBulk,
onUpdateConversations,
};
@@ -38,7 +38,7 @@ describe('useAutomation', () => {
});
useMapGetter.mockImplementation(getter => {
const getterMap = {
'agents/getAgents': agents,
'agents/getVerifiedAgents': agents,
'campaigns/getAllCampaigns': campaigns,
'contacts/getContacts': contacts,
'inboxes/getInboxes': inboxes,
@@ -111,7 +111,7 @@ describe('useMacros', () => {
useStoreGetters.mockReturnValue({
'labels/getLabels': { value: mockLabels },
'teams/getTeams': { value: mockTeams },
'agents/getAgents': { value: mockAgents },
'agents/getVerifiedAgents': { value: mockAgents },
});
});
@@ -167,7 +167,7 @@ describe('useMacros', () => {
useStoreGetters.mockReturnValue({
'labels/getLabels': { value: [] },
'teams/getTeams': { value: [] },
'agents/getAgents': { value: [] },
'agents/getVerifiedAgents': { value: [] },
});
const { getMacroDropdownValues } = useMacros();
@@ -1,4 +1,4 @@
import { ref, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
@@ -40,7 +40,7 @@ export function useAutomation(startValue = null) {
} = useAutomationValues();
const automation = ref(startValue);
const automationTypes = structuredClone(AUTOMATIONS);
const automationTypes = reactive(structuredClone(AUTOMATIONS));
const eventName = computed(() => automation.value?.event_name);
/**
@@ -160,14 +160,24 @@ export function useAutomation(startValue = null) {
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
);
const CUSTOM_ATTR_HEADER_KEYS = new Set([
'conversation_custom_attribute',
'contact_custom_attribute',
]);
[
'message_created',
'conversation_created',
'conversation_updated',
'conversation_opened',
].forEach(eventToUpdate => {
const standardConditions = automationTypes[
eventToUpdate
].conditions.filter(
c => !c.customAttributeType && !CUSTOM_ATTR_HEADER_KEYS.has(c.key)
);
automationTypes[eventToUpdate].conditions = [
...automationTypes[eventToUpdate].conditions,
...standardConditions,
...manifestedCustomAttributes,
];
});
@@ -20,7 +20,7 @@ import {
export default function useAutomationValues() {
const getters = useStoreGetters();
const { t } = useI18n();
const agents = useMapGetter('agents/getAgents');
const agents = useMapGetter('agents/getVerifiedAgents');
const campaigns = useMapGetter('campaigns/getAllCampaigns');
const contacts = useMapGetter('contacts/getContacts');
const inboxes = useMapGetter('inboxes/getInboxes');
@@ -11,6 +11,7 @@ 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();
@@ -69,7 +70,10 @@ export function useCaptain() {
* @param {Error} error - The error object from the API call.
*/
const handleAPIError = error => {
if (error.name === 'AbortError' || error.name === 'CanceledError') {
if (
error.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
error.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
) {
return;
}
const errorMessage =
@@ -78,6 +82,24 @@ export function useCaptain() {
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.
@@ -103,7 +125,7 @@ export function useCaptain() {
return { message: generatedMessage, followUpContext };
} catch (error) {
handleAPIError(error);
return { message: '' };
return { message: '', errorType: getErrorType(error) };
}
};
@@ -125,7 +147,7 @@ export function useCaptain() {
return { message: generatedMessage, followUpContext };
} catch (error) {
handleAPIError(error);
return { message: '' };
return { message: '', errorType: getErrorType(error) };
}
};
@@ -147,7 +169,7 @@ export function useCaptain() {
return { message: generatedMessage, followUpContext };
} catch (error) {
handleAPIError(error);
return { message: '' };
return { message: '', errorType: getErrorType(error) };
}
};
@@ -171,7 +193,11 @@ export function useCaptain() {
return { message: generatedMessage, followUpContext: updatedContext };
} catch (error) {
handleAPIError(error);
return { message: '', followUpContext };
return {
message: '',
followUpContext,
errorType: getErrorType(error),
};
}
};
@@ -3,6 +3,10 @@ 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 = [
@@ -52,6 +56,20 @@ function buildPayload(action, conversationId, followUpCount = undefined) {
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.
@@ -146,7 +164,8 @@ export function useCopilotReply() {
// Reset without tracking dismiss (starting new action)
reset(false);
abortController.value = new AbortController();
const requestController = new AbortController();
abortController.value = requestController;
isGenerating.value = true;
isContentReady.value = false;
currentAction.value = action;
@@ -154,28 +173,66 @@ export function useCopilotReply() {
trackedConversationId.value = conversationId.value;
try {
const { message: content, followUpContext: newContext } =
await processEvent(action, data, {
signal: abortController.value.signal,
});
const {
message: content,
followUpContext: newContext,
errorType,
} = await processEvent(action, data, {
signal: requestController.signal,
});
if (!abortController.value?.signal.aborted) {
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)
);
if (requestController.signal.aborted) return;
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
if (abortController.value === requestController) {
isGenerating.value = false;
}
isGenerating.value = false;
return;
}
} catch {
if (!abortController.value?.signal.aborted) {
isGenerating.value = false;
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;
}
}
}
@@ -187,7 +244,8 @@ export function useCopilotReply() {
async function sendFollowUp(message) {
if (!followUpContext.value || !message.trim()) return;
abortController.value = new AbortController();
const requestController = new AbortController();
abortController.value = requestController;
isGenerating.value = true;
isContentReady.value = false;
@@ -198,24 +256,65 @@ export function useCopilotReply() {
followUpCount.value += 1;
try {
const { message: content, followUpContext: updatedContext } =
await followUp({
followUpContext: followUpContext.value,
message,
signal: abortController.value.signal,
});
const {
message: content,
followUpContext: updatedContext,
errorType,
} = await followUp({
followUpContext: followUpContext.value,
message,
signal: requestController.signal,
});
if (!abortController.value?.signal.aborted) {
if (content) {
generatedContent.value = content;
followUpContext.value = updatedContext;
showEditor.value = true;
if (requestController.signal.aborted) return;
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
if (abortController.value === requestController) {
isGenerating.value = false;
}
isGenerating.value = false;
return;
}
} catch {
if (!abortController.value?.signal.aborted) {
isGenerating.value = false;
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;
}
}
}
@@ -13,7 +13,7 @@ export const useMacros = () => {
const labels = computed(() => getters['labels/getLabels'].value);
const teams = computed(() => getters['teams/getTeams'].value);
const agents = computed(() => getters['agents/getAgents'].value);
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
/**
* Get dropdown values based on the specified type