@webex/contact-center 3.12.0-task-refactor.5 → 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/TaskUtils.js +8 -6
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +71 -13
- 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 +69 -7
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1 -1
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +60 -8
- package/dist/types/services/task/state-machine/guards.d.ts +5 -0
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskUtils.ts +8 -6
- package/src/services/task/state-machine/TaskStateMachine.ts +94 -15
- 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 +150 -9
- package/src/services/task/voice/Voice.ts +4 -1
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/task/TaskUtils.ts +16 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +512 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +929 -0
- package/test/unit/spec/services/task/voice/Voice.ts +20 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -134,11 +134,11 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
134
134
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
135
135
|
target: TaskState.CONSULTING,
|
|
136
136
|
actions: [
|
|
137
|
-
'updateTaskData',
|
|
138
137
|
'setConsultInitiator',
|
|
139
138
|
'setConsultDestination',
|
|
140
139
|
'setConsultFromConference',
|
|
141
140
|
'setConsultAgentJoined',
|
|
141
|
+
'updateTaskData',
|
|
142
142
|
'emitTaskConsultAccepted',
|
|
143
143
|
'emitTaskConsulting',
|
|
144
144
|
],
|
|
@@ -204,8 +204,9 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
204
204
|
{
|
|
205
205
|
target: TaskState.CONSULTING,
|
|
206
206
|
actions: [
|
|
207
|
-
'updateTaskData',
|
|
208
207
|
'setConsultAgentJoined',
|
|
208
|
+
'setConsultDestination',
|
|
209
|
+
'updateTaskData',
|
|
209
210
|
'emitTaskConsultAccepted',
|
|
210
211
|
'emitTaskConsulting',
|
|
211
212
|
],
|
|
@@ -232,15 +233,25 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
232
233
|
|
|
233
234
|
[TaskState.CONNECTED]: {
|
|
234
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
|
+
},
|
|
235
246
|
// AgentConsulting may arrive while machine is CONNECTED (EP-DN/event ordering).
|
|
236
247
|
// Derive consultInitiator from payload so controls are set correctly.
|
|
237
248
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
238
249
|
target: TaskState.CONSULTING,
|
|
239
250
|
actions: [
|
|
240
|
-
'updateTaskData',
|
|
241
251
|
'setConsultInitiator',
|
|
242
252
|
'setConsultDestination',
|
|
243
253
|
'setConsultAgentJoined',
|
|
254
|
+
'updateTaskData',
|
|
244
255
|
'emitTaskConsultAccepted',
|
|
245
256
|
'emitTaskConsulting',
|
|
246
257
|
],
|
|
@@ -329,6 +340,22 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
329
340
|
|
|
330
341
|
[TaskState.HELD]: {
|
|
331
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
|
+
},
|
|
332
359
|
// Click of the unhold button
|
|
333
360
|
[TaskEvent.UNHOLD_INITIATED]: {
|
|
334
361
|
target: TaskState.RESUME_INITIATING,
|
|
@@ -448,9 +475,9 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
448
475
|
// AgentConsulting updates consulted agent arrival
|
|
449
476
|
[TaskEvent.CONSULTING_ACTIVE]: {
|
|
450
477
|
actions: [
|
|
451
|
-
'updateTaskData',
|
|
452
478
|
'setConsultAgentJoined',
|
|
453
479
|
'setConsultDestination',
|
|
480
|
+
'updateTaskData',
|
|
454
481
|
'emitTaskConsulting',
|
|
455
482
|
],
|
|
456
483
|
},
|
|
@@ -458,14 +485,33 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
458
485
|
// AgentConsultEnded
|
|
459
486
|
[TaskEvent.CONSULT_END]: [
|
|
460
487
|
{
|
|
461
|
-
// Initiator returning to conference
|
|
488
|
+
// Initiator returning to conference only while conference is still active.
|
|
462
489
|
guard: ({context, event}) =>
|
|
463
490
|
context.consultInitiator === true &&
|
|
464
|
-
(context
|
|
465
|
-
guards.conferenceInProgressFromEvent({context, event})),
|
|
491
|
+
guards.conferenceInProgressFromEvent({context, event}),
|
|
466
492
|
target: TaskState.CONFERENCING,
|
|
467
493
|
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
468
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
|
+
},
|
|
469
515
|
{
|
|
470
516
|
// Initiator already switched back to the main/customer leg
|
|
471
517
|
guard: ({context}) =>
|
|
@@ -474,13 +520,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
474
520
|
actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
|
|
475
521
|
},
|
|
476
522
|
{
|
|
477
|
-
//
|
|
523
|
+
// Interaction terminated during consult (customer left) → WRAPPING_UP
|
|
478
524
|
guard: ({context, event}) => {
|
|
525
|
+
// if (context.consultInitiator !== true) return false;
|
|
526
|
+
// const taskData = getTaskDataFromEvent(event);
|
|
479
527
|
const taskData = getTaskDataFromEvent(event);
|
|
480
528
|
const cpd = taskData?.interaction?.callProcessingDetails;
|
|
481
529
|
if (cpd?.hasCustomerLeft !== 'true') return false;
|
|
482
530
|
|
|
483
|
-
return
|
|
531
|
+
return (
|
|
532
|
+
taskData?.interaction?.isTerminated === true &&
|
|
533
|
+
shouldWrapUpForThisAgent(context, taskData)
|
|
534
|
+
);
|
|
484
535
|
},
|
|
485
536
|
target: TaskState.WRAPPING_UP,
|
|
486
537
|
actions: [
|
|
@@ -698,12 +749,40 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
698
749
|
},
|
|
699
750
|
],
|
|
700
751
|
|
|
701
|
-
[TaskEvent.HOLD_SUCCESS]:
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
+
],
|
|
707
786
|
|
|
708
787
|
// Start a new consult from within an active conference
|
|
709
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
|
*
|