Skip to content
31 changes: 24 additions & 7 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,7 @@ function renderTaskList(taskList) {
const isNew = isIncomingTask(task, agentId);
const isTelephony = task.data.interaction.mediaType === 'telephony';
const isBrowserPhone = agentDeviceType === 'BROWSER';
const isAutoAnswering = task.data.isAutoAnswering || false;

// Determine which buttons to show
const showAcceptButton = isNew && (isBrowserPhone || !isTelephony);
Expand All @@ -2136,8 +2137,8 @@ function renderTaskList(taskList) {
taskElement.innerHTML = `
<div class="task-item-content">
<p>${callerDisplay}</p>
${showAcceptButton ? `<button class="accept-task" data-task-id="${taskId}">Accept</button>` : ''}
${showDeclineButton ? `<button class="decline-task" data-task-id="${taskId}">Decline</button>` : ''}
${showAcceptButton ? `<button class="accept-task" data-task-id="${taskId}" ${isAutoAnswering ? 'disabled' : ''}>Accept</button>` : ''}
${showDeclineButton ? `<button class="decline-task" data-task-id="${taskId}" ${isAutoAnswering ? 'disabled' : ''}>Decline</button>` : ''}
</div>
<hr class="task-separator">
`;
Expand Down Expand Up @@ -2208,24 +2209,40 @@ function renderTaskList(taskList) {
function enableAnswerDeclineButtons(task) {
const callerDisplay = task.data.interaction?.callAssociatedDetails?.ani;
const isNew = isIncomingTask(task, agentId);
const chatAndSocial = ['chat', 'social'];
const isAutoAnswering = task.data.isAutoAnswering || false;
const chatAndSocial = ['chat', 'social'];

if (task.data.interaction.mediaType === 'telephony') {
if (agentDeviceType === 'BROWSER') {
answerElm.disabled = !isNew;
declineElm.disabled = !isNew;
// Disable buttons if auto-answering or not new
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = !isNew || isAutoAnswering;

incomingDetailsElm.innerText = `Call from ${callerDisplay}`;

// Log auto-answer status for debugging
if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
} else {
incomingDetailsElm.innerText = `Call from ${callerDisplay}...please answer on the endpoint where the agent's extension is registered`;
}
} else if (chatAndSocial.includes(task.data.interaction.mediaType)) {
answerElm.disabled = !isNew;
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = true;
incomingDetailsElm.innerText = `Chat from ${callerDisplay}`;

if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
} else if (task.data.interaction.mediaType === 'email') {
answerElm.disabled = !isNew;
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = true;
incomingDetailsElm.innerText = `Email from ${callerDisplay}`;

if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/@webex/contact-center/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
// TODO: Make profile a singleton to make it available throughout app/sdk so we dont need to inject info everywhere
this.taskManager.setWrapupData(this.agentConfig.wrapUpData);
this.taskManager.setAgentId(this.agentConfig.agentId);
this.taskManager.setWebRtcEnabled(this.agentConfig.webRtcEnabled);

if (
this.agentConfig.webRtcEnabled &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isParticipantInMainInteraction,
isPrimary,
isSecondaryEpDnAgent,
shouldAutoAnswerTask,
} from './TaskUtils';

/** @internal */
Expand All @@ -36,6 +37,7 @@ export default class TaskManager extends EventEmitter {
private static taskManager;
private wrapupData: WrapupData;
private agentId: string;
private webRtcEnabled: boolean;
/**
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
* @param webCallingService - Webrtc Service Layer
Expand Down Expand Up @@ -73,6 +75,10 @@ export default class TaskManager extends EventEmitter {
return this.agentId;
}

public setWebRtcEnabled(webRtcEnabled: boolean) {
this.webRtcEnabled = webRtcEnabled;
}

private handleIncomingWebCall = (call: ICall) => {
const currentTask = Object.values(this.taskCollection).find(
(task) => task.data.interaction.mediaType === 'telephony'
Expand Down Expand Up @@ -130,6 +136,14 @@ export default class TaskManager extends EventEmitter {
interactionId: payload.data.interactionId,
});

// Check if auto-answer should happen for this task
const shouldAutoAnswer = shouldAutoAnswerTask(
payload.data,
this.agentId,
this.webCallingService.loginOption,
this.webRtcEnabled
);

task = new Task(
this.contact,
this.webCallingService,
Expand All @@ -138,6 +152,7 @@ export default class TaskManager extends EventEmitter {
wrapUpRequired:
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
isConferenceInProgress: getIsConferenceInProgress(payload.data),
isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
},
this.wrapupData,
this.agentId
Expand Down Expand Up @@ -169,13 +184,22 @@ export default class TaskManager extends EventEmitter {
}
break;

case CC_EVENTS.AGENT_CONTACT_RESERVED:
case CC_EVENTS.AGENT_CONTACT_RESERVED: {
// Check if auto-answer should happen for this task
const shouldAutoAnswerReserved = shouldAutoAnswerTask(
payload.data,
this.agentId,
this.webCallingService.loginOption,
this.webRtcEnabled
);

task = new Task(
this.contact,
this.webCallingService,
{
...payload.data,
isConsulted: false,
isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
},
this.wrapupData,
this.agentId
Expand All @@ -190,6 +214,7 @@ export default class TaskManager extends EventEmitter {
this.emit(TASK_EVENTS.TASK_INCOMING, task);
}
break;
}
case CC_EVENTS.AGENT_OFFER_CONTACT:
// We don't have to emit any event here since this will be result of promise.
task = this.updateTaskData(task, payload.data);
Expand All @@ -199,6 +224,9 @@ export default class TaskManager extends EventEmitter {
interactionId: payload.data?.interactionId,
});
this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);

// Handle auto-answer for offer contact
this.handleAutoAnswer(task);
break;
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
// We don't have to emit any event here since this will be result of promise.
Expand Down Expand Up @@ -301,6 +329,9 @@ export default class TaskManager extends EventEmitter {
isConsulted: true, // This ensures that the task is marked as us being requested for a consult
});
task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);

// Handle auto-answer for consult offer
this.handleAutoAnswer(task);
break;
case CC_EVENTS.AGENT_CONSULTING:
// Received when agent is in an active consult state
Expand Down Expand Up @@ -538,6 +569,47 @@ export default class TaskManager extends EventEmitter {
}
}

/**
* Handles auto-answer logic for incoming tasks
* Automatically accepts tasks when isAutoAnswering flag is set
* The flag is set during task creation based on:
* 1. WebRTC calls with auto-answer enabled in agent profile
* 2. Agent-initiated WebRTC outdial calls
* 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
*
* @param task - The task to auto-answer
* @private
*/
private async handleAutoAnswer(task: ITask): Promise<void> {
if (!task || !task.data || !task.data.isAutoAnswering) {
return;
}

LoggerProxy.info(`Auto-answering task`, {
module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
});

try {
await task.accept();
LoggerProxy.info(`Task auto-answered successfully`, {
module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
});
} catch (error) {
// Reset isAutoAnswering flag on failure
task.updateTaskData({...task.data, isAutoAnswering: false});
LoggerProxy.error(`Failed to auto-answer task`, {
module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
error,
});
}
}

/**
* Handles cleanup of task resources including Desktop/WebRTC call cleanup and task removal
* @param task - The task to clean up
Expand Down
110 changes: 109 additions & 1 deletion packages/@webex/contact-center/src/services/task/TaskUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable import/prefer-default-export */
import {Interaction, ITask, TaskData} from './types';
import {Interaction, ITask, TaskData, MEDIA_CHANNEL} from './types';
import {OUTDIAL_DIRECTION, OUTDIAL_MEDIA_TYPE, OUTBOUND_TYPE} from '../../constants';
import {LoginOption} from '../../types';

/**
* Determines if the given agent is the primary agent (owner) of the task
Expand Down Expand Up @@ -111,3 +113,109 @@ export const isSecondaryEpDnAgent = (interaction: Interaction): boolean => {

return interaction.mediaType === 'telephony' && isSecondaryAgent(interaction);
};

/**
* Checks if auto-answer is enabled for the agent participant
* @param interaction - The interaction object
* @param agentId - Current agent ID
* @returns true if auto-answer is enabled, false otherwise
*/
export const isAutoAnswerEnabled = (interaction: Interaction, agentId: string): boolean => {
return interaction.participants?.[agentId]?.autoAnswerEnabled === true;
};

/**
* Checks if the interaction is a WebRTC call eligible for auto-answer
* @param interaction - The interaction object
* @param loginOption - The agent's login option (BROWSER, AGENT_DN, etc.)
* @param webRtcEnabled - Whether WebRTC is enabled for the agent
* @returns true if this is a WebRTC call, false otherwise
*/
export const isWebRTCCall = (
interaction: Interaction,
loginOption: string,
webRtcEnabled: boolean
): boolean => {
return (
webRtcEnabled &&
loginOption === LoginOption.BROWSER &&
interaction.mediaType === OUTDIAL_MEDIA_TYPE
);
};

/**
* Checks if the interaction is a digital outbound (Email/SMS)
* @param interaction - The interaction object
* @returns true if this is a digital outbound, false otherwise
*/
export const isDigitalOutbound = (interaction: Interaction): boolean => {
return (
interaction.contactDirection?.type === OUTDIAL_DIRECTION &&
interaction.outboundType === OUTBOUND_TYPE &&
(interaction.mediaChannel === MEDIA_CHANNEL.EMAIL ||
interaction.mediaChannel === MEDIA_CHANNEL.SMS)
);
};

/**
* Checks if the outdial was initiated by the current agent
* @param interaction - The interaction object
* @param agentId - Current agent ID
* @returns true if agent initiated the outdial, false otherwise
*/
export const isAgentInitiatedOutdial = (interaction: Interaction, agentId: string): boolean => {
return (
interaction.contactDirection?.type === OUTDIAL_DIRECTION &&
interaction.outboundType === OUTBOUND_TYPE &&
interaction.callProcessingDetails?.outdialAgentId === agentId &&
interaction.owner === agentId &&
!interaction.callProcessingDetails?.BLIND_TRANSFER_IN_PROGRESS
);
};

/**
* Determines if a task should be auto-answered based on interaction data
* Auto-answer logic handles:
* 1. WebRTC calls with auto-answer enabled in agent profile
* 2. Agent-initiated WebRTC outdial calls
* 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
*
* @param taskData - The task data
* @param agentId - Current agent ID
* @param loginOption - Agent's login option
* @param webRtcEnabled - Whether WebRTC is enabled for the agent
* @returns true if task should be auto-answered, false otherwise
*/
export const shouldAutoAnswerTask = (
taskData: TaskData,
agentId: string,
loginOption: string,
webRtcEnabled: boolean
): boolean => {
const {interaction} = taskData;

if (!interaction || !agentId) {
return false;
}

// Check if auto-answer is enabled for this agent
const autoAnswerEnabled = isAutoAnswerEnabled(interaction, agentId);

// Check if this is an agent-initiated outdial
const agentInitiatedOutdial = isAgentInitiatedOutdial(interaction, agentId);

// WebRTC telephony calls
if (isWebRTCCall(interaction, loginOption, webRtcEnabled)) {
return autoAnswerEnabled || agentInitiatedOutdial;
}

// Digital outbound (Email/SMS)
if (isDigitalOutbound(interaction) && agentInitiatedOutdial) {
// Don't auto-answer if task has been transferred (has previous vteams)
const hasPreviousVteams = interaction.previousVTeams && interaction.previousVTeams.length > 0;

return !hasPreviousVteams;
}

return false;
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const PRESERVED_TASK_DATA_FIELDS = {
WRAP_UP_REQUIRED: 'wrapUpRequired',
/** Indicates if a conference is currently in progress (2+ active agents) */
IS_CONFERENCE_IN_PROGRESS: 'isConferenceInProgress',
/** Indicates if auto-answer is in progress for this task */
IS_AUTO_ANSWERING: 'isAutoAnswering',
};

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/@webex/contact-center/src/services/task/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ export type Interaction = {
BLIND_TRANSFER_IN_PROGRESS?: boolean;
/** Desktop view configuration for Flow Control */
fcDesktopView?: string;
/** Agent ID who initiated the outdial call */
outdialAgentId?: string;
};
/** Main interaction identifier for related interactions */
mainInteractionId?: string;
Expand Down Expand Up @@ -785,6 +787,8 @@ export type TaskData = {
reservedAgentChannelId?: string;
/** Indicates if wrap-up is required for this task */
wrapUpRequired?: boolean;
/** Indicates if auto-answer is in progress for this task */
isAutoAnswering?: boolean;
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/@webex/contact-center/test/unit/spec/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ describe('webex.cc', () => {
task: undefined,
setWrapupData: jest.fn(),
setAgentId: jest.fn(),
setWebRtcEnabled: jest.fn(),
registerIncomingCallEvent: jest.fn(),
registerTaskListeners: jest.fn(),
getTask: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2105,5 +2105,6 @@ describe('TaskManager', () => {
);
});
});

});

Loading
Loading