@webex/contact-center 3.12.0-task-refactor.4 → 3.12.0-task-refactor.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/task/TaskManager.js +1 -0
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +8 -6
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +77 -14
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +85 -13
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +35 -0
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +76 -10
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/voice/Voice.js +10 -4
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +68 -8
- package/dist/types/services/task/state-machine/guards.d.ts +5 -0
- package/dist/types/services/task/voice/Voice.d.ts +18 -17
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskManager.ts +1 -1
- package/src/services/task/TaskUtils.ts +8 -6
- package/src/services/task/state-machine/TaskStateMachine.ts +101 -16
- package/src/services/task/state-machine/actions.ts +148 -24
- package/src/services/task/state-machine/guards.ts +46 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +158 -15
- package/src/services/task/voice/Voice.ts +12 -5
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/task/TaskManager.ts +26 -0
- package/test/unit/spec/services/task/TaskUtils.ts +16 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +573 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +1023 -46
- package/test/unit/spec/services/task/voice/Voice.ts +44 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
package/package.json
CHANGED
|
@@ -291,7 +291,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
291
291
|
return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
|
|
292
292
|
|
|
293
293
|
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
|
|
294
|
-
return {type: TaskEvent.OUTBOUND_FAILED, reason: payload.reason};
|
|
294
|
+
return {type: TaskEvent.OUTBOUND_FAILED, taskData: payload, reason: payload.reason};
|
|
295
295
|
|
|
296
296
|
case CC_EVENTS.CONTACT_RECORDING_STARTED:
|
|
297
297
|
return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
|
|
@@ -13,13 +13,14 @@ import {TaskContext} from './state-machine/types';
|
|
|
13
13
|
* @returns true if customer is in the call
|
|
14
14
|
*/
|
|
15
15
|
export const getIsCustomerInCall = (interaction: Interaction, interactionId: string): boolean => {
|
|
16
|
-
const mainCallMedia = interaction.media[interactionId];
|
|
17
|
-
|
|
16
|
+
const mainCallMedia = interaction.media?.[interactionId];
|
|
17
|
+
const participants = interaction.participants;
|
|
18
|
+
if (!mainCallMedia?.participants || !participants) {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
return mainCallMedia.participants.some((participantId: string) => {
|
|
22
|
-
const participant =
|
|
23
|
+
const participant = participants[participantId];
|
|
23
24
|
|
|
24
25
|
return participant?.pType === PARTICIPANT_TYPE.CUSTOMER && !participant.hasLeft;
|
|
25
26
|
});
|
|
@@ -37,14 +38,15 @@ export const getConferenceParticipantsCount = (
|
|
|
37
38
|
interaction: Interaction,
|
|
38
39
|
interactionId: string
|
|
39
40
|
): number => {
|
|
40
|
-
const mainCallMedia = interaction.media[interactionId];
|
|
41
|
-
|
|
41
|
+
const mainCallMedia = interaction.media?.[interactionId];
|
|
42
|
+
const participants = interaction.participants;
|
|
43
|
+
if (!mainCallMedia?.participants || !participants) {
|
|
42
44
|
return 0;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
let count = 0;
|
|
46
48
|
for (const participantId of mainCallMedia.participants) {
|
|
47
|
-
const participant =
|
|
49
|
+
const participant = participants[participantId];
|
|
48
50
|
if (
|
|
49
51
|
participant &&
|
|
50
52
|
participant.pType !== PARTICIPANT_TYPE.CUSTOMER &&
|
|
@@ -123,16 +123,22 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
123
123
|
actions: ['initializeTask', 'emitTaskIncoming'],
|
|
124
124
|
},
|
|
125
125
|
|
|
126
|
+
// AgentOutboundFailed can arrive before TASK_INCOMING due to race conditions
|
|
127
|
+
[TaskEvent.OUTBOUND_FAILED]: {
|
|
128
|
+
target: TaskState.TERMINATED,
|
|
129
|
+
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
130
|
+
},
|
|
131
|
+
|
|
126
132
|
// EP-DN split-leg ordering can deliver AgentConsulting before HYDRATE/TASK_INCOMING.
|
|
127
133
|
// Do not drop it in IDLE; bootstrap to CONSULTING using event taskData.
|
|
128
134
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
129
135
|
target: TaskState.CONSULTING,
|
|
130
136
|
actions: [
|
|
131
|
-
'updateTaskData',
|
|
132
137
|
'setConsultInitiator',
|
|
133
138
|
'setConsultDestination',
|
|
134
139
|
'setConsultFromConference',
|
|
135
140
|
'setConsultAgentJoined',
|
|
141
|
+
'updateTaskData',
|
|
136
142
|
'emitTaskConsultAccepted',
|
|
137
143
|
'emitTaskConsulting',
|
|
138
144
|
],
|
|
@@ -190,7 +196,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
190
196
|
},
|
|
191
197
|
{
|
|
192
198
|
target: TaskState.TERMINATED,
|
|
193
|
-
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', '
|
|
199
|
+
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
194
200
|
},
|
|
195
201
|
],
|
|
196
202
|
// AgentConsulting comes for received after the initial consult is accepted
|
|
@@ -198,8 +204,9 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
198
204
|
{
|
|
199
205
|
target: TaskState.CONSULTING,
|
|
200
206
|
actions: [
|
|
201
|
-
'updateTaskData',
|
|
202
207
|
'setConsultAgentJoined',
|
|
208
|
+
'setConsultDestination',
|
|
209
|
+
'updateTaskData',
|
|
203
210
|
'emitTaskConsultAccepted',
|
|
204
211
|
'emitTaskConsulting',
|
|
205
212
|
],
|
|
@@ -226,15 +233,25 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
226
233
|
|
|
227
234
|
[TaskState.CONNECTED]: {
|
|
228
235
|
on: {
|
|
236
|
+
// AgentConsultConferenced / ParticipantJoinedConference can arrive while connected.
|
|
237
|
+
[TaskEvent.CONFERENCE_START]: {
|
|
238
|
+
target: TaskState.CONFERENCING,
|
|
239
|
+
actions: [
|
|
240
|
+
'updateTaskData',
|
|
241
|
+
'syncTaskDataFromEvent',
|
|
242
|
+
'clearConsultState',
|
|
243
|
+
'emitTaskConferenceStarted',
|
|
244
|
+
],
|
|
245
|
+
},
|
|
229
246
|
// AgentConsulting may arrive while machine is CONNECTED (EP-DN/event ordering).
|
|
230
247
|
// Derive consultInitiator from payload so controls are set correctly.
|
|
231
248
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
232
249
|
target: TaskState.CONSULTING,
|
|
233
250
|
actions: [
|
|
234
|
-
'updateTaskData',
|
|
235
251
|
'setConsultInitiator',
|
|
236
252
|
'setConsultDestination',
|
|
237
253
|
'setConsultAgentJoined',
|
|
254
|
+
'updateTaskData',
|
|
238
255
|
'emitTaskConsultAccepted',
|
|
239
256
|
'emitTaskConsulting',
|
|
240
257
|
],
|
|
@@ -323,6 +340,22 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
323
340
|
|
|
324
341
|
[TaskState.HELD]: {
|
|
325
342
|
on: {
|
|
343
|
+
// Conference can be merged while this agent is in held state after refresh/recovery.
|
|
344
|
+
[TaskEvent.CONFERENCE_START]: {
|
|
345
|
+
target: TaskState.CONFERENCING,
|
|
346
|
+
actions: [
|
|
347
|
+
'updateTaskData',
|
|
348
|
+
'syncTaskDataFromEvent',
|
|
349
|
+
'clearConsultState',
|
|
350
|
+
'emitTaskConferenceStarted',
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
[TaskEvent.PAUSE_RECORDING]: {
|
|
354
|
+
actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingPaused'],
|
|
355
|
+
},
|
|
356
|
+
[TaskEvent.RESUME_RECORDING]: {
|
|
357
|
+
actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingResumed'],
|
|
358
|
+
},
|
|
326
359
|
// Click of the unhold button
|
|
327
360
|
[TaskEvent.UNHOLD_INITIATED]: {
|
|
328
361
|
target: TaskState.RESUME_INITIATING,
|
|
@@ -442,9 +475,9 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
442
475
|
// AgentConsulting updates consulted agent arrival
|
|
443
476
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
444
477
|
actions: [
|
|
445
|
-
'updateTaskData',
|
|
446
478
|
'setConsultAgentJoined',
|
|
447
479
|
'setConsultDestination',
|
|
480
|
+
'updateTaskData',
|
|
448
481
|
'emitTaskConsulting',
|
|
449
482
|
],
|
|
450
483
|
},
|
|
@@ -452,14 +485,33 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
452
485
|
// AgentConsultEnded
|
|
453
486
|
[TaskEvent.CONSULT_END]: [
|
|
454
487
|
{
|
|
455
|
-
// Initiator returning to conference
|
|
488
|
+
// Initiator returning to conference only while conference is still active.
|
|
456
489
|
guard: ({context, event}) =>
|
|
457
490
|
context.consultInitiator === true &&
|
|
458
|
-
(context
|
|
459
|
-
guards.conferenceInProgressFromEvent({context, event})),
|
|
491
|
+
guards.conferenceInProgressFromEvent({context, event}),
|
|
460
492
|
target: TaskState.CONFERENCING,
|
|
461
493
|
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
462
494
|
},
|
|
495
|
+
{
|
|
496
|
+
// Conference consult ended after conference downgrade while main leg is held.
|
|
497
|
+
guard: ({context, event}) =>
|
|
498
|
+
context.consultInitiator === true &&
|
|
499
|
+
context.consultFromConference === true &&
|
|
500
|
+
!guards.conferenceInProgressFromEvent({context, event}) &&
|
|
501
|
+
guards.isConferenceHoldParticipantFromEvent({context, event}),
|
|
502
|
+
target: TaskState.HELD,
|
|
503
|
+
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
// Conference consult ended after conference downgrade while main leg is connected.
|
|
507
|
+
guard: ({context, event}) =>
|
|
508
|
+
context.consultInitiator === true &&
|
|
509
|
+
context.consultFromConference === true &&
|
|
510
|
+
!guards.conferenceInProgressFromEvent({context, event}) &&
|
|
511
|
+
!guards.isConferenceHoldParticipantFromEvent({context, event}),
|
|
512
|
+
target: TaskState.CONNECTED,
|
|
513
|
+
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
514
|
+
},
|
|
463
515
|
{
|
|
464
516
|
// Initiator already switched back to the main/customer leg
|
|
465
517
|
guard: ({context}) =>
|
|
@@ -468,13 +520,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
468
520
|
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
469
521
|
},
|
|
470
522
|
{
|
|
471
|
-
//
|
|
523
|
+
// Interaction terminated during consult (customer left) → WRAPPING_UP
|
|
472
524
|
guard: ({context, event}) => {
|
|
525
|
+
// if (context.consultInitiator !== true) return false;
|
|
526
|
+
// const taskData = getTaskDataFromEvent(event);
|
|
473
527
|
const taskData = getTaskDataFromEvent(event);
|
|
474
528
|
const cpd = taskData?.interaction?.callProcessingDetails;
|
|
475
529
|
if (cpd?.hasCustomerLeft !== 'true') return false;
|
|
476
530
|
|
|
477
|
-
return
|
|
531
|
+
return (
|
|
532
|
+
taskData?.interaction?.isTerminated === true &&
|
|
533
|
+
shouldWrapUpForThisAgent(context, taskData)
|
|
534
|
+
);
|
|
478
535
|
},
|
|
479
536
|
target: TaskState.WRAPPING_UP,
|
|
480
537
|
actions: [
|
|
@@ -692,12 +749,40 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
692
749
|
},
|
|
693
750
|
],
|
|
694
751
|
|
|
695
|
-
[TaskEvent.HOLD_SUCCESS]:
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
752
|
+
[TaskEvent.HOLD_SUCCESS]: [
|
|
753
|
+
{
|
|
754
|
+
// Conference already downgraded (no other agents) and backend hold arrives.
|
|
755
|
+
// Move to HELD so the UI renders resume action.
|
|
756
|
+
guard: guards.shouldDowngradeConferenceToConnected,
|
|
757
|
+
target: TaskState.HELD,
|
|
758
|
+
actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
[TaskEvent.UNHOLD_SUCCESS]: [
|
|
765
|
+
{
|
|
766
|
+
// Conference already downgraded (no other agents) and backend unhold arrives.
|
|
767
|
+
// Move to CONNECTED so hold action is available again.
|
|
768
|
+
guard: guards.shouldDowngradeConferenceToConnected,
|
|
769
|
+
target: TaskState.CONNECTED,
|
|
770
|
+
actions: [
|
|
771
|
+
'updateTaskData',
|
|
772
|
+
'syncTaskDataFromEvent',
|
|
773
|
+
'setHoldState',
|
|
774
|
+
'emitTaskResume',
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
actions: [
|
|
779
|
+
'updateTaskData',
|
|
780
|
+
'syncTaskDataFromEvent',
|
|
781
|
+
'setHoldState',
|
|
782
|
+
'emitTaskResume',
|
|
783
|
+
],
|
|
784
|
+
},
|
|
785
|
+
],
|
|
701
786
|
|
|
702
787
|
// Start a new consult from within an active conference
|
|
703
788
|
[TaskEvent.CONSULT]: {
|
|
@@ -11,9 +11,16 @@ import {
|
|
|
11
11
|
TaskActionArgs,
|
|
12
12
|
RecordingStateUpdate,
|
|
13
13
|
} from './types';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
TaskEvent,
|
|
16
|
+
TaskState,
|
|
17
|
+
INTERACTION_STATE,
|
|
18
|
+
CONSULT_STATE,
|
|
19
|
+
MEDIA_TYPE_CONSULT,
|
|
20
|
+
} from './constants';
|
|
15
21
|
import {DestinationType, TaskData} from '../types';
|
|
16
22
|
import {computeUIControls, getDefaultUIControls} from './uiControlsComputer';
|
|
23
|
+
import {getIsConferenceInProgress} from '../TaskUtils';
|
|
17
24
|
import {hasActiveConsultInPostCall} from './guards';
|
|
18
25
|
|
|
19
26
|
const determineConsultInitiator = (
|
|
@@ -74,6 +81,113 @@ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate
|
|
|
74
81
|
return update;
|
|
75
82
|
};
|
|
76
83
|
|
|
84
|
+
const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: string): boolean => {
|
|
85
|
+
if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true;
|
|
86
|
+
if (selfAgentId) {
|
|
87
|
+
const selfParticipant = taskData?.interaction?.participants?.[selfAgentId] as any;
|
|
88
|
+
const hasConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some(
|
|
89
|
+
(media: any) => media?.mType === MEDIA_TYPE_CONSULT
|
|
90
|
+
);
|
|
91
|
+
const isPendingOrActiveSelfConsult =
|
|
92
|
+
selfParticipant?.consultState === CONSULT_STATE.CONSULTING ||
|
|
93
|
+
selfParticipant?.consultState === 'consultInitiated';
|
|
94
|
+
if (isPendingOrActiveSelfConsult && hasConsultMedia && taskData?.isConsulted === false) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL && selfAgentId) {
|
|
99
|
+
const selfParticipant = taskData.interaction?.participants?.[selfAgentId] as any;
|
|
100
|
+
const hasConsultMedia = Object.values(taskData.interaction?.media ?? {}).some(
|
|
101
|
+
(media: any) => media?.mType === MEDIA_TYPE_CONSULT
|
|
102
|
+
);
|
|
103
|
+
if (selfParticipant?.consultState === CONSULT_STATE.CONSULTING && hasConsultMedia) return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const isSelfConsultingOrPending = (
|
|
110
|
+
taskData: TaskData | undefined,
|
|
111
|
+
selfAgentId?: string
|
|
112
|
+
): boolean => {
|
|
113
|
+
if (!taskData || !selfAgentId) return false;
|
|
114
|
+
const selfParticipant = taskData?.interaction?.participants?.[selfAgentId] as any;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
selfParticipant?.consultState === CONSULT_STATE.CONSULTING ||
|
|
118
|
+
selfParticipant?.consultState === 'consultInitiated'
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const hasJoinedConsultDestination = (taskData: TaskData | undefined): boolean => {
|
|
123
|
+
if (!taskData?.interaction) return false;
|
|
124
|
+
const participants = taskData.interaction.participants as any;
|
|
125
|
+
const cpd = taskData.interaction.callProcessingDetails as any;
|
|
126
|
+
const backendSaysJoined = cpd?.consultDestinationAgentJoined === 'true';
|
|
127
|
+
if (backendSaysJoined) return true;
|
|
128
|
+
if (!participants) return false;
|
|
129
|
+
|
|
130
|
+
return Object.values(participants).some((p: any) => {
|
|
131
|
+
if (!p || p.isConsulted !== true || p.hasLeft) return false;
|
|
132
|
+
|
|
133
|
+
return p.hasJoined === true || p.consultState === CONSULT_STATE.CONSULTING;
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const deriveConsultCallHeldFromTaskData = (taskData: TaskData | undefined): boolean | undefined => {
|
|
138
|
+
if (!taskData?.interaction) return undefined;
|
|
139
|
+
|
|
140
|
+
const eventType = taskData.type;
|
|
141
|
+
if (eventType === 'AgentContactHeld') return true;
|
|
142
|
+
if (eventType === 'AgentContactUnheld') return false;
|
|
143
|
+
|
|
144
|
+
const consultMediaId = taskData.consultMediaResourceId;
|
|
145
|
+
|
|
146
|
+
const consultMedia: any = consultMediaId
|
|
147
|
+
? taskData.interaction.media?.[consultMediaId]
|
|
148
|
+
: Object.values(taskData.interaction.media ?? {}).find(
|
|
149
|
+
(m: any) => m?.mType === MEDIA_TYPE_CONSULT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!consultMedia) return undefined;
|
|
153
|
+
|
|
154
|
+
return Boolean(consultMedia.isHold);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const getTaskStateForUiControls = (
|
|
158
|
+
taskData: TaskData | undefined,
|
|
159
|
+
selfAgentId: string | undefined
|
|
160
|
+
): TaskState => {
|
|
161
|
+
if (!taskData?.interaction) {
|
|
162
|
+
return TaskState.IDLE;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (taskData.interaction.isTerminated === true) {
|
|
166
|
+
return TaskState.WRAPPING_UP;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isActiveConsultState(taskData, selfAgentId)) {
|
|
170
|
+
return TaskState.CONSULTING;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
taskData.interaction.state === INTERACTION_STATE.CONFERENCE ||
|
|
175
|
+
getIsConferenceInProgress(taskData)
|
|
176
|
+
) {
|
|
177
|
+
return TaskState.CONFERENCING;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mainMediaId = taskData.interaction.mainInteractionId || taskData.interactionId;
|
|
181
|
+
const isMainHeld = Boolean(
|
|
182
|
+
mainMediaId && taskData.interaction.media?.[mainMediaId]?.isHold === true
|
|
183
|
+
);
|
|
184
|
+
if (taskData.interaction.state === 'hold' || isMainHeld) {
|
|
185
|
+
return TaskState.HELD;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return TaskState.CONNECTED;
|
|
189
|
+
};
|
|
190
|
+
|
|
77
191
|
const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefined) =>
|
|
78
192
|
taskData
|
|
79
193
|
? (() => {
|
|
@@ -84,8 +198,14 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
|
|
|
84
198
|
|
|
85
199
|
const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId;
|
|
86
200
|
const consultingActive =
|
|
87
|
-
taskData
|
|
201
|
+
isActiveConsultState(taskData, selfAgentId) ||
|
|
88
202
|
hasActiveConsultInPostCall(taskData, selfAgentId);
|
|
203
|
+
const conferenceFromPayload =
|
|
204
|
+
taskData?.interaction?.state === INTERACTION_STATE.CONFERENCE ||
|
|
205
|
+
getIsConferenceInProgress(taskData);
|
|
206
|
+
const selfConsultingOrPending = isSelfConsultingOrPending(taskData, selfAgentId);
|
|
207
|
+
const inferredConsultingInitiator =
|
|
208
|
+
selfConsultingOrPending && taskData?.isConsulted === false;
|
|
89
209
|
|
|
90
210
|
if (taskData.destAgentId) {
|
|
91
211
|
const isEpDnWithStoredId =
|
|
@@ -102,24 +222,26 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
|
|
|
102
222
|
const consultInitiator = determineConsultInitiator(taskData, selfAgentId);
|
|
103
223
|
if (consultInitiator !== undefined) {
|
|
104
224
|
updates.consultInitiator = consultInitiator;
|
|
105
|
-
} else if (
|
|
225
|
+
} else if (
|
|
226
|
+
inferredConsultingInitiator ||
|
|
227
|
+
(consultingActive && taskData.isConsulted === false)
|
|
228
|
+
) {
|
|
106
229
|
updates.consultInitiator = true;
|
|
107
230
|
}
|
|
108
231
|
}
|
|
109
232
|
|
|
233
|
+
const effectiveConsultInitiator = updates.consultInitiator ?? context.consultInitiator;
|
|
234
|
+
if (
|
|
235
|
+
effectiveConsultInitiator &&
|
|
236
|
+
conferenceFromPayload &&
|
|
237
|
+
(consultingActive || selfConsultingOrPending || Boolean(taskData?.consultMediaResourceId))
|
|
238
|
+
) {
|
|
239
|
+
updates.consultFromConference = true;
|
|
240
|
+
}
|
|
241
|
+
|
|
110
242
|
if (consultingActive && taskData.interaction) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
taskData.interaction.participants &&
|
|
114
|
-
Object.values(taskData.interaction.participants).some(
|
|
115
|
-
(p: any) => p?.isConsulted === true && !p?.hasLeft
|
|
116
|
-
)
|
|
117
|
-
);
|
|
118
|
-
const cpd = taskData.interaction?.callProcessingDetails;
|
|
119
|
-
const backendSaysJoined = cpd?.consultDestinationAgentJoined === 'true';
|
|
120
|
-
if (hasJoinedConsultee || backendSaysJoined)
|
|
121
|
-
updates.consultDestinationAgentJoined = true;
|
|
122
|
-
}
|
|
243
|
+
const joinedConsultee = hasJoinedConsultDestination(taskData);
|
|
244
|
+
if (joinedConsultee) updates.consultDestinationAgentJoined = true;
|
|
123
245
|
|
|
124
246
|
if (!context.consultDestinationType && !updates.consultDestinationType) {
|
|
125
247
|
const hasEpDnParticipant = Boolean(
|
|
@@ -131,20 +253,22 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
|
|
|
131
253
|
if (hasEpDnParticipant) updates.consultDestinationType = 'entryPoint' as any;
|
|
132
254
|
}
|
|
133
255
|
|
|
134
|
-
const effectiveConsultInitiator = updates.consultInitiator ?? context.consultInitiator;
|
|
135
256
|
if (effectiveConsultInitiator) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
: Object.values(taskData.interaction.media ?? {}).find(
|
|
140
|
-
(m: any) => m?.mType === 'consult'
|
|
141
|
-
);
|
|
142
|
-
if (consultMedia) {
|
|
143
|
-
updates.consultCallHeld = Boolean(consultMedia.isHold);
|
|
257
|
+
const consultCallHeld = deriveConsultCallHeldFromTaskData(taskData);
|
|
258
|
+
if (consultCallHeld !== undefined) {
|
|
259
|
+
updates.consultCallHeld = consultCallHeld;
|
|
144
260
|
}
|
|
145
261
|
}
|
|
146
262
|
}
|
|
147
263
|
|
|
264
|
+
const nextContext = {
|
|
265
|
+
...context,
|
|
266
|
+
...updates,
|
|
267
|
+
} as TaskContext;
|
|
268
|
+
const inferredState = getTaskStateForUiControls(taskData, selfAgentId);
|
|
269
|
+
|
|
270
|
+
updates.uiControls = computeUIControls(inferredState, nextContext, taskData);
|
|
271
|
+
|
|
148
272
|
return updates;
|
|
149
273
|
})()
|
|
150
274
|
: {};
|
|
@@ -120,6 +120,20 @@ export const guards = {
|
|
|
120
120
|
|
|
121
121
|
if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true;
|
|
122
122
|
|
|
123
|
+
// Hydrate can report interaction as conference/hold while this agent is actively consulting.
|
|
124
|
+
// Detect consult by participant consultState + consult media, independent of top-level state.
|
|
125
|
+
const selfAgentId = getSelfAgentId(context, taskData);
|
|
126
|
+
const selfParticipant = selfAgentId ? taskData?.interaction?.participants?.[selfAgentId] : null;
|
|
127
|
+
const hasConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some(
|
|
128
|
+
(media: any) => media?.mType === MEDIA_TYPE_CONSULT
|
|
129
|
+
);
|
|
130
|
+
const selfActiveConsult = selfParticipant?.consultState === CONSULT_STATE.CONSULTING;
|
|
131
|
+
const selfPendingConsultInitiated =
|
|
132
|
+
selfParticipant?.consultState === 'consultInitiated' && taskData?.isConsulted === false;
|
|
133
|
+
if ((selfActiveConsult || selfPendingConsultInitiated) && hasConsultMedia) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
// EP_DN consulted agent: backend reports state as 'connected' but CPD indicates consult
|
|
124
138
|
const cpd = taskData?.interaction?.callProcessingDetails;
|
|
125
139
|
if (
|
|
@@ -135,6 +149,24 @@ export const guards = {
|
|
|
135
149
|
return true;
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
// Customer left during consult: interaction state is "post_call" but consult
|
|
153
|
+
// between agents is still active. Detect via agent's consultState + consult media.
|
|
154
|
+
if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL) {
|
|
155
|
+
const postCallSelfAgentId = getSelfAgentId(context, taskData);
|
|
156
|
+
const postCallSelfParticipant = postCallSelfAgentId
|
|
157
|
+
? taskData?.interaction?.participants?.[postCallSelfAgentId]
|
|
158
|
+
: null;
|
|
159
|
+
const hasPostCallConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some(
|
|
160
|
+
(media: any) => media?.mType === MEDIA_TYPE_CONSULT
|
|
161
|
+
);
|
|
162
|
+
if (
|
|
163
|
+
postCallSelfParticipant?.consultState === CONSULT_STATE.CONSULTING &&
|
|
164
|
+
hasPostCallConsultMedia
|
|
165
|
+
) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
return false;
|
|
139
171
|
},
|
|
140
172
|
|
|
@@ -183,6 +215,20 @@ export const guards = {
|
|
|
183
215
|
return getIsConferenceInProgress(taskData);
|
|
184
216
|
},
|
|
185
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Conference hold signal for conference-downgraded consult-end transitions.
|
|
220
|
+
* Backend can report this as boolean or string.
|
|
221
|
+
*/
|
|
222
|
+
isConferenceHoldParticipantFromEvent: ({context, event}: GuardParams): boolean => {
|
|
223
|
+
const taskData = getTaskDataFromEvent(event) ?? context.taskData;
|
|
224
|
+
const callProcessingDetails = taskData?.interaction?.callProcessingDetails as
|
|
225
|
+
| {conferenceHoldParticipant?: boolean | string}
|
|
226
|
+
| undefined;
|
|
227
|
+
const conferenceHoldParticipant = callProcessingDetails?.conferenceHoldParticipant;
|
|
228
|
+
|
|
229
|
+
return conferenceHoldParticipant === true || conferenceHoldParticipant === 'true';
|
|
230
|
+
},
|
|
231
|
+
|
|
186
232
|
/**
|
|
187
233
|
* Conference downgrade check specifically for transitioning back to CONNECTED.
|
|
188
234
|
*
|