Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![license: Cisco](https://img.shields.io/badge/License-Cisco-blueviolet?style=flat-square)](https://github.com/webex/webex-js-sdk/blob/master/LICENSE)
![state: Stable](https://img.shields.io/badge/State-Stable-blue?style=flat-square)
![scope: Public](https://img.shields.io/badge/Scope-Public-darkgreen?style=flat-square)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/webex/webex-js-sdk)

This project is designed as a mono-repository for all publicly-provided JavaScript packages from Cisco's Webex Developer Experience team. These packages consist of mostly API-related modules that allow for seamless integration with the collection of services that belong to the Webex platform.

Expand Down
91 changes: 77 additions & 14 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const initiateConsultDialog = document.querySelector('#initiate-consult-dialog')
const agentMultiLoginAlert = document.querySelector('#agentMultiLoginAlert');
const consultTransferBtn = document.querySelector('#consult-transfer');
const transferElm = document.getElementById('transfer');
const transferOptionsElm = document.querySelector('#transfer-options');
const conferenceToggleBtn = document.querySelector('#conference-toggle');
const timerElm = document.querySelector('#timerDisplay');
const engageElm = document.querySelector('#engageWidget');
Expand Down Expand Up @@ -649,6 +650,16 @@ async function handleQueueConsult(consultPayload) {
}


// Function to toggle transfer options visibility
function toggleTransferOptions() {
if (transferOptionsElm.style.display === 'none') {
transferOptionsElm.style.display = 'block';
onTransferTypeSelectionChanged(); // Refresh the destination options
} else {
transferOptionsElm.style.display = 'none';
}
}

// Function to initiate transfer
async function initiateTransfer() {
const destinationType = document.querySelector('#transfer-destination-type').value;
Expand All @@ -667,6 +678,7 @@ async function initiateTransfer() {
try {
await currentTask.transfer(transferPayload);
console.log('Transfer initiated successfully');
transferOptionsElm.style.display = 'none';
} catch (error) {
console.error('Failed to initiate transfer', error);
alert('Failed to initiate transfer');
Expand All @@ -692,7 +704,7 @@ async function initiateConsultTransfer() {
if (currentTask.data.isConferenceInProgress) {
await currentTask.transferConference();
} else {
await currentTask.consultTransfer(consultTransferPayload);
await currentTask.transfer(consultTransferPayload);
console.log('Consult transfer initiated successfully');
}
} catch (error) {
Expand Down Expand Up @@ -919,13 +931,16 @@ async function startOutdial() {
try {
console.log('Making an outdial call');
console.log('Destination:', destination);
console.log('Selected ANI:', selectedAni || 'None selected');
console.log('Selected ANI:', selectedAni || 'None selected, using default ANI');

// Use selected ANI as the origin parameter
if (selectedAni) {
await webex.cc.startOutdial(destination, selectedAni);
console.log('Outdial call initiated successfully with ANI:', selectedAni);
}
} else {
await webex.cc.startOutdial(destination);
console.log('Outdial call initiated successfully with default ANI');
}

} catch (error) {
console.error('Failed to initiate outdial call', error);
Expand Down Expand Up @@ -1013,6 +1028,12 @@ function registerTaskListeners(task) {
showAgentStatePopup(reason);
});

task.on('task:outdialFailed', (reason) => {
updateTaskList();
console.info('Outdial failed with reason:', reason);
showOutdialFailedPopup(reason);
});

task.on('task:wrappedup', updateTaskList); // Update the task list UI to have latest tasks

// Conference event listeners - Simplified approach
Expand Down Expand Up @@ -1128,12 +1149,12 @@ function getConsultStatus(task) {
const participant = Object.values(participants).find(p => p.pType === 'Agent' && p.id === agentId);

if (state === 'consult') {
if (participant && participant.isConsulted) {
if ((participant && participant.isConsulted )|| isSecondaryEpDnAgent(task)) {
return 'beingConsulted';
}
return 'consultInitiated';
} else if (state === 'consulting') {
if (participant && participant.isConsulted) {
if ((participant && participant.isConsulted) || isSecondaryEpDnAgent(task)) {
return 'beingConsultedAccepted';
}
return 'consultAccepted';
Expand Down Expand Up @@ -1819,6 +1840,31 @@ function showAgentStatePopup(reason) {
popup.classList.remove('hidden');
}

function showOutdialFailedPopup(reason) {
const outdialFailedReasonText = document.getElementById('outdialFailedReasonText');

// Set the reason text based on the reason
if (reason === 'CUSTOMER_BUSY') {
outdialFailedReasonText.innerText = 'Customer is busy';
} else if (reason === 'NO_ANSWER') {
outdialFailedReasonText.innerText = 'No answer from customer';
} else if (reason === 'CALL_FAILED') {
outdialFailedReasonText.innerText = 'Call failed';
} else if (reason === 'INVALID_NUMBER') {
outdialFailedReasonText.innerText = 'Invalid phone number';
} else {
outdialFailedReasonText.innerText = `Outdial failed: ${reason}`;
}

const outdialFailedPopup = document.getElementById('outdialFailedPopup');
outdialFailedPopup.classList.remove('hidden');
}

function closeOutdialFailedPopup() {
const outdialFailedPopup = document.getElementById('outdialFailedPopup');
outdialFailedPopup.classList.add('hidden');
}

async function renderBuddyAgents() {
buddyAgentsDropdownElm.innerHTML = ''; // Clear previous options
const buddyAgentsDropdownNodes = await fetchBuddyAgentsNodeList();
Expand Down Expand Up @@ -1955,14 +2001,14 @@ function expandAll() {
function holdResumeCall() {
if (holdResumeElm.innerText === 'Hold') {
holdResumeElm.disabled = true;
currentTask.hold().then(() => {
currentTask.holdResume().then(() => {
console.info('Call held successfully');
}).catch((error) => {
console.error('Failed to hold the call', error);
});
} else {
holdResumeElm.disabled = true;
currentTask.resume().then(() => {
currentTask.holdResume().then(() => {
console.info('Call resumed successfully');
}).catch((error) => {
console.error('Failed to resume the call', error);
Expand Down Expand Up @@ -2127,6 +2173,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 +2183,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 +2255,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
9 changes: 9 additions & 0 deletions docs/samples/contact-center/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ <h2>Set the state of the agent</h2>
</div>
</div>

<!-- Popup for outdial failed -->
<div id="outdialFailedPopup" class="modal hidden">
<div class="modal-content">
<h2>Outdial Failed</h2>
<p id="outdialFailedReasonText"></p>
<button id="closeOutdialFailedPopup" class="btn btn-primary my-3" onclick="closeOutdialFailedPopup()">Close</button>
</div>
</div>

<style>
.modal {
position: fixed;
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const packages = [
'@webex/internal-plugin-encryption',
'@webex/internal-plugin-feature',
'@webex/internal-plugin-flag',
'@webex/internal-plugin-task',
'@webex/internal-plugin-scheduler',
'@webex/internal-plugin-llm',
'@webex/internal-plugin-locus',
Expand Down
13 changes: 11 additions & 2 deletions karma-ng.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,21 @@ function makeConfig(packageName, argv) {
'integration',
'spec',
'**',
'*.js'
'*.{js,ts}'
);
const unitTestPath = path.join(
'packages',
packageName,
'test',
'unit',
'spec',
'**',
'*.{js,ts}'
);
const unitTestPath = path.join('packages', packageName, 'test', 'unit', 'spec', '**', '*.js');

const preprocessors = {
'packages/**': ['browserify'],
'packages/**/*.ts': ['browserify'],
// 'packages/**/*.ts': ['tsify', 'browserify']
};

Expand Down
23 changes: 22 additions & 1 deletion packages/@webex/contact-center/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
UploadLogsResponse,
UpdateDeviceTypeResponse,
GenericError,
ConfigFlags,
} from './types';
import {
READY,
Expand Down Expand Up @@ -317,7 +318,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
* });
* ```
*/
private queue: Queue;
public queue: Queue;

/**
* Logger utility for Contact Center plugin
Expand Down Expand Up @@ -396,6 +397,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.trigger(TASK_EVENTS.TASK_HYDRATE, task);
};

/**
* Handles task merged events when tasks are combined eg: EPDN merge/transfer
* @private
* @param {ITask} task The task object that has been merged
*/
private handleTaskMerged = (task: ITask) => {
// @ts-ignore
this.trigger(TASK_EVENTS.TASK_MERGED, task);
};

/**
* Sets up event listeners for incoming tasks and task hydration
* Subscribes to task events from the task manager
Expand All @@ -404,6 +415,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
private incomingTaskListener() {
this.taskManager.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
this.taskManager.on(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
this.taskManager.on(TASK_EVENTS.TASK_MERGED, this.handleTaskMerged);
}

/**
Expand Down Expand Up @@ -695,9 +707,18 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
module: CC_FILE,
method: METHODS.CONNECT_WEBSOCKET,
});

const configFlags: ConfigFlags = {
isEndCallEnabled: this.agentConfig.isEndCallEnabled,
isEndConsultEnabled: this.agentConfig.isEndConsultEnabled,
webRtcEnabled: this.agentConfig.webRtcEnabled,
autoWrapup: this.agentConfig.wrapUpData?.wrapUpProps?.autoWrapup ?? false,
};
this.taskManager.setConfigFlags(configFlags);
// 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
7 changes: 6 additions & 1 deletion packages/@webex/contact-center/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ export const METHODS = {
HANDLE_DEVICE_TYPE: 'handleDeviceType',
START_OUTDIAL: 'startOutdial',
GET_QUEUES: 'getQueues',
GET_OUTDIAL_ANI_ENTRIES: 'getOutdialAniEntries',
UPLOAD_LOGS: 'uploadLogs',
UPDATE_AGENT_PROFILE: 'updateAgentProfile',
GET_DEVICE_ID: 'getDeviceId',
HANDLE_INCOMING_TASK: 'handleIncomingTask',
HANDLE_TASK_HYDRATE: 'handleTaskHydrate',
INCOMING_TASK_LISTENER: 'incomingTaskListener',
HOLD_RESUME: 'holdResume',
ACCEPT: 'accept',
REJECT: 'reject',
TRANSFER_CALL: 'transferCall',
COMPLETE_TRANSFER: 'completeTransfer',
GET_OUTDIAL_ANI_ENTRIES: 'getOutdialAniEntries',
};
1 change: 1 addition & 0 deletions packages/@webex/contact-center/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export type {
AgentContact,
/** Task interface */
ITask,
Interaction,
TaskData,
/** Task response */
TaskResponse,
Expand Down
12 changes: 12 additions & 0 deletions packages/@webex/contact-center/src/metrics/behavioral-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,18 @@ const eventTaxonomyMap: Record<string, BehavioralEventTaxonomy> = {
target: 'task_accept_consult',
verb: 'fail',
},
[METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS]: {
product,
agent: 'user',
target: 'task_auto_answer',
verb: 'complete',
},
[METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED]: {
product,
agent: 'user',
target: 'task_auto_answer',
verb: 'fail',
},
[METRIC_EVENT_NAMES.TASK_OUTDIAL_SUCCESS]: {
product,
agent: 'user',
Expand Down
9 changes: 6 additions & 3 deletions packages/@webex/contact-center/src/metrics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type Enum<T extends Record<string, unknown>> = T[keyof T];
* @property {string} TASK_PAUSE_RECORDING_FAILED - Event name for failed pause of recording.
* @property {string} TASK_ACCEPT_CONSULT_SUCCESS - Event name for successful consult acceptance.
* @property {string} TASK_ACCEPT_CONSULT_FAILED - Event name for failed consult acceptance.
* @property {string} TASK_AUTO_ANSWER_SUCCESS - Event name for successful auto-answer.
* @property {string} TASK_AUTO_ANSWER_FAILED - Event name for failed auto-answer.
*
* @property {string} TASK_CONFERENCE_START_SUCCESS - Event name for successful conference start.
* @property {string} TASK_CONFERENCE_START_FAILED - Event name for failed conference start.
Expand Down Expand Up @@ -117,6 +119,8 @@ export const METRIC_EVENT_NAMES = {
TASK_PAUSE_RECORDING_FAILED: 'Task Pause Recording Failed',
TASK_ACCEPT_CONSULT_SUCCESS: 'Task Accept Consult Success',
TASK_ACCEPT_CONSULT_FAILED: 'Task Accept Consult Failed',
TASK_AUTO_ANSWER_SUCCESS: 'Task Auto Answer Success',
TASK_AUTO_ANSWER_FAILED: 'Task Auto Answer Failed',

// Conference Tasks
TASK_CONFERENCE_START_SUCCESS: 'Task Conference Start Success',
Expand All @@ -134,11 +138,10 @@ export const METRIC_EVENT_NAMES = {
WEBSOCKET_DEREGISTER_SUCCESS: 'Websocket Deregister Success',
WEBSOCKET_DEREGISTER_FAIL: 'Websocket Deregister Failed',

// WebSocket message events
WEBSOCKET_EVENT_RECEIVED: 'Websocket Event Received',

AGENT_DEVICE_TYPE_UPDATE_SUCCESS: 'Agent Device Type Update Success',
AGENT_DEVICE_TYPE_UPDATE_FAILED: 'Agent Device Type Update Failed',
// WebSocket message events
WEBSOCKET_EVENT_RECEIVED: 'Websocket Event Received',

// EntryPoint API Events
ENTRYPOINT_FETCH_SUCCESS: 'Entrypoint Fetch Success',
Expand Down
Loading
Loading