Skip to content

Commit

Permalink
feat(plugin-cc): implementation of tasks in agent (#4016)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kesari3008 authored Dec 12, 2024
1 parent 3fa2938 commit 755f62f
Show file tree
Hide file tree
Showing 30 changed files with 2,070 additions and 221 deletions.
68 changes: 65 additions & 3 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ let deviceId;
let agentStatusId;
let agentStatus;
let agentId;
let taskControl;
let task;
let taskId;

const authTypeElm = document.querySelector('#auth-type');
const credentialsFormElm = document.querySelector('#credentials');
Expand All @@ -27,13 +30,18 @@ const authStatusElm = document.querySelector('#access-token-status');
const registerBtn = document.querySelector('#webexcc-register');
const teamsDropdown = document.querySelector('#teamsDropdown');
const agentLogin = document.querySelector('#AgentLogin');
const agentLoginButton = document.querySelector('#loginAgent');
const loginAgentElm = document.querySelector('#loginAgent');
const dialNumber = document.querySelector('#dialNumber');
const registerStatus = document.querySelector('#ws-connection-status');
const idleCodesDropdown = document.querySelector('#idleCodesDropdown')
const setAgentStatusButton = document.querySelector('#setAgentStatus');
const logoutAgentElm = document.querySelector('#logoutAgent');
const buddyAgentsDropdownElm = document.getElementById('buddyAgentsDropdown');
const incomingCallListener = document.querySelector('#incomingsection');
const incomingDetailsElm = document.querySelector('#incoming-call');
const answerElm = document.querySelector('#answer');
const declineElm = document.querySelector('#decline');
const callControlListener = document.querySelector('#callcontrolsection');

// Store and Grab `access-token` from sessionStorage
if (sessionStorage.getItem('date') > new Date().getTime()) {
Expand Down Expand Up @@ -70,6 +78,22 @@ function toggleDisplay(elementId, status) {
}
}

const taskEvents = new CustomEvent('task:incoming', {
detail: {
task: task,
},
});

// TODO: Activate the call control buttons once the call is accepted and refctor this
function registerTaskListeners(task) {
task.on('task:assigned', (task) => {
console.log('Call has been accepted for task: ', task.data.interactionId);
})
task.on('task:media', (track) => {
document.getElementById('remote-audio').srcObject = new MediaStream([track]);
})
}

function generateWebexConfig({credentials}) {
return {
appName: 'sdk-samples',
Expand Down Expand Up @@ -132,7 +156,7 @@ function register() {
agentLogin.innerHTML = '<option value="" selected>Choose Agent Login ...</option>'; // Clear previously selected option on agentLogin.
dialNumber.value = agentProfile.defaultDn ? agentProfile.defaultDn : '';
dialNumber.disabled = agentProfile.defaultDn ? false : true;
if(loginVoiceOptions.length > 0) agentLoginButton.disabled = false;
if (loginVoiceOptions.length > 0) loginAgentElm.disabled = false;
loginVoiceOptions.forEach((voiceOptions)=> {
const option = document.createElement('option');
option.text = voiceOptions;
Expand All @@ -157,10 +181,15 @@ function register() {
idleCodesDropdown.add(option);
}
});

}).catch((error) => {
console.error('Event subscription failed', error);
})

webex.cc.on('task:incoming', (task) => {
taskEvents.detail.task = task;

incomingCallListener.dispatchEvent(taskEvents);
})
}

async function handleAgentLogin(e) {
Expand All @@ -178,6 +207,7 @@ async function handleAgentLogin(e) {
function doAgentLogin() {
webex.cc.stationLogin({teamId: teamsDropdown.value, loginOption: agentDeviceType, dialNumber: dialNumber.value}).then((response) => {
console.log('Agent Logged in successfully', response);
loginAgentElm.disabled = true;
logoutAgentElm.classList.remove('hidden');
}
).catch((error) => {
Expand Down Expand Up @@ -205,6 +235,7 @@ function setAgentStatus() {
function logoutAgent() {
webex.cc.stationLogout({logoutReason: 'logout'}).then((response) => {
console.log('Agent logged out successfully', response);
loginAgentElm.disabled = false;

setTimeout(() => {
logoutAgentElm.classList.add('hidden');
Expand Down Expand Up @@ -247,6 +278,37 @@ async function fetchBuddyAgents() {
}
}

incomingCallListener.addEventListener('task:incoming', (event) => {
task = event.detail.task;
taskId = event.detail.task.data.interactionId;

const callerDisplay = event.detail.task.data.interaction.callAssociatedDetails.ani;
registerTaskListeners(task);

if (task.webCallingService.loginOption === 'BROWSER') {
answerElm.disabled = false;
declineElm.disabled = false;

incomingDetailsElm.innerText = `Call from ${callerDisplay}`;
} else {
incomingDetailsElm.innerText = `Call from ${callerDisplay}...please answer on the endpoint where the agent's extension is registered`;
}
});

function answer() {
answerElm.disabled = true;
declineElm.disabled = true;
task.accept(taskId);
incomingDetailsElm.innerText = 'Call Accepted';
}

function decline() {
answerElm.disabled = true;
declineElm.disabled = true;
task.decline(taskId);
incomingDetailsElm.innerText = 'No incoming calls';
}

const allCollapsibleElements = document.querySelectorAll('.collapsible');
allCollapsibleElements.forEach((el) => {
el.addEventListener('click', (event) => {
Expand Down
29 changes: 26 additions & 3 deletions docs/samples/contact-center/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ <h2 class="collapsible">
<option value="" selected hidden>Choose Agent Login ...</option>
</select>
<input id="dialNumber" name="dialNumber" placeholder="Dial Number" value="" type="text">
<button id="loginAgent" disabled class="btn btn-primary my-3" onclick="doAgentLogin()">Login With
Selected Team</button>
<button id="logoutAgent" class="btn btn-primary my-3 ml-2 hidden" onclick="logoutAgent()">Logout Agent</button>
<button id="loginAgent" disabled class="btn btn-primary my-3" onclick="doAgentLogin()">Login</button>
<button id="logoutAgent" class="btn btn-primary my-3 ml-2 hidden" onclick="logoutAgent()">Logout</button>
</fieldset>
<fieldset>
<legend>Agent status</legend>
Expand All @@ -131,6 +130,30 @@ <h2 class="collapsible">
</div>
</fieldset>
</div>
<!-- calling / incoming -->
<div id="incomingsection">
<fieldset>
<legend>Incoming Call</legend>
<div class="u-mv">
<pre id="incoming-call"> No Incoming Calls</pre>
<button onclick="answer()" disabled="" id="answer" class="btn--green">Answer</button>
<button onclick="decline()" disabled="" id="decline" class="btn--red">Decline</button>
</div>
</fieldset>
<fieldset>
<legend>Remote Audio</legend>
<audio id="remote-audio" autoplay></audio>
</fieldset>
</div>
<!-- Agent Call Controls -->
<div id="callcontrolsection">
<fieldset>
<legend>Call Controls</legend>
<div class="u-mv">
<!-- Add the call control buttons here -->
</div>
</fieldset>
</div>
</section>
</div>
</div>
Expand Down
35 changes: 33 additions & 2 deletions packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {WebexPlugin} from '@webex/webex-core';
import EventEmitter from 'events';
import {
SetStateResponse,
CCPluginConfig,
Expand All @@ -14,7 +15,6 @@ import {
SubscribeRequest,
} from './types';
import {READY, CC_FILE, EMPTY_STRING} from './constants';
import WebCallingService from './services/WebCallingService';
import {AGENT, WEB_RTC_PREFIX} from './services/constants';
import Services from './services';
import HttpRequest from './services/core/HttpRequest';
Expand All @@ -23,20 +23,26 @@ import {StateChange, Logout} from './services/agent/types';
import {getErrorDetails} from './services/core/Utils';
import {Profile, WelcomeEvent} from './services/config/types';
import {AGENT_STATE_AVAILABLE} from './services/config/constants';
import {ConnectionLostDetails} from './services/core/WebSocket/types';
import {ConnectionLostDetails} from './services/core/websocket/types';
import TaskManager from './services/task/TaskManager';
import WebCallingService from './services/WebCallingService';
import {ITask, TASK_EVENTS} from './services/task/types';

export default class ContactCenter extends WebexPlugin implements IContactCenter {
namespace = 'cc';
private $config: CCPluginConfig;
private $webex: WebexSDK;
private eventEmitter: EventEmitter;
private agentConfig: Profile;
private webCallingService: WebCallingService;
private services: Services;
private httpRequest: HttpRequest;
private taskManager: TaskManager;

constructor(...args) {
super(...args);

this.eventEmitter = new EventEmitter();
// @ts-ignore
this.$webex = this.webex;

Expand All @@ -57,11 +63,28 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
});

this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig);
this.taskManager = TaskManager.getTaskManager(
this.services.contact,
this.webCallingService,
this.services.webSocketManager
);

LoggerProxy.initialize(this.$webex.logger);
});
}

private handleIncomingTask = (task: ITask) => {
// @ts-ignore
this.trigger(TASK_EVENTS.TASK_INCOMING, task);
};

/**
* An Incoming Call listener.
*/
private incomingTaskListener() {
this.taskManager.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
}

/**
* This is used for making the CC SDK ready by setting up the cc mercury connection.
*/
Expand Down Expand Up @@ -167,8 +190,13 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
await this.webCallingService.registerWebCallingLine();
}

this.webCallingService.setLoginOption(data.loginOption);

await loginResponse;

this.incomingTaskListener();
this.taskManager.registerIncomingCallEvent();

return loginResponse;
} catch (error) {
const {error: detailedError} = getErrorDetails(error, 'stationLogin', CC_FILE);
Expand All @@ -193,6 +221,9 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.webCallingService.deregisterWebCallingLine();
}

this.taskManager.unregisterIncomingCallEvent();
this.taskManager.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);

return logoutResponse;
} catch (error) {
const {error: detailedError} = getErrorDetails(error, 'stationLogout', CC_FILE);
Expand Down
89 changes: 77 additions & 12 deletions packages/@webex/plugin-cc/src/services/WebCallingService.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
import EventEmitter from 'events';
import {
createClient,
ICall,
ICallingClient,
ILine,
LINE_EVENTS,
CallingClientConfig,
LocalMicrophoneStream,
CALL_EVENT_KEYS,
} from '@webex/calling';
import {WebexSDK} from '../types';
import {LoginOption, WebexSDK} from '../types';
import {TIMEOUT_DURATION, WEB_CALLING_SERVICE_FILE} from '../constants';
import LoggerProxy from '../logger-proxy';

export default class WebCallingService {
export default class WebCallingService extends EventEmitter {
private callingClient: ICallingClient;
private callingClientConfig: CallingClientConfig;
private line: ILine;
private call: ICall;
private webex: WebexSDK;
public loginOption: LoginOption;
constructor(webex: WebexSDK, callingClientConfig: CallingClientConfig) {
super();
this.webex = webex;
this.callingClientConfig = callingClientConfig;
}

public setLoginOption(loginOption: LoginOption) {
this.loginOption = loginOption;
}

private handleMediaEvent = (track: MediaStreamTrack) => {
this.emit(CALL_EVENT_KEYS.REMOTE_MEDIA, track);
};

private registerCallListeners() {
// TODO: Add remaining call listeners here
this.call.on(CALL_EVENT_KEYS.REMOTE_MEDIA, this.handleMediaEvent);
}

public unregisterCallListeners() {
// TODO: Once we handle disconnect or call end, switch off the call listeners
this.call.off(CALL_EVENT_KEYS.REMOTE_MEDIA, this.handleMediaEvent);
}

public async registerWebCallingLine(): Promise<void> {
this.callingClient = await createClient(this.webex as any, this.callingClientConfig);
this.line = Object.values(this.callingClient.getLines())[0];
Expand All @@ -33,16 +56,9 @@ export default class WebCallingService {
});

// Start listening for incoming calls
this.line.on(LINE_EVENTS.INCOMING_CALL, (callObj: ICall) => {
this.call = callObj;

const incomingCallEvent = new CustomEvent(LINE_EVENTS.INCOMING_CALL, {
detail: {
call: this.call,
},
});

window.dispatchEvent(incomingCallEvent);
this.line.on(LINE_EVENTS.INCOMING_CALL, (call: ICall) => {
this.call = call;
this.emit(LINE_EVENTS.INCOMING_CALL, call);
});

return new Promise<void>((resolve, reject) => {
Expand All @@ -65,4 +81,53 @@ export default class WebCallingService {
public async deregisterWebCallingLine() {
this.line?.deregister();
}

public answerCall(localAudioStream: LocalMicrophoneStream, taskId: string) {
if (this.call) {
try {
this.webex.logger.info(`Call answered: ${taskId}`);
this.call.answer(localAudioStream);
this.registerCallListeners();
} catch (error) {
this.webex.logger.error(`Failed to answer call for ${taskId}. Error: ${error}`);
// Optionally, throw the error to allow the invoker to handle it
throw error;
}
} else {
this.webex.logger.log(`Cannot answer a non WebRtc Call: ${taskId}`);
}
}

public muteCall(localAudioStream: LocalMicrophoneStream) {
if (this.call) {
this.webex.logger.info('Call mute or unmute requested!');
this.call.mute(localAudioStream);
} else {
this.webex.logger.log(`Cannot mute a non WebRtc Call`);
}
}

public isCallMuted() {
if (this.call) {
return this.call.isMuted();
}

return false;
}

public declineCall(taskId: string) {
if (this.call) {
try {
this.webex.logger.info(`Call end requested: ${taskId}`);
this.call.end();
this.unregisterCallListeners();
} catch (error) {
this.webex.logger.error(`Failed to end call: ${taskId}. Error: ${error}`);
// Optionally, throw the error to allow the invoker to handle it
throw error;
}
} else {
this.webex.logger.log(`Cannot end a non WebRtc Call: ${taskId}`);
}
}
}
Loading

0 comments on commit 755f62f

Please sign in to comment.