@webex/contact-center 3.12.0-next.73 → 3.12.0-next.74

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.
Files changed (188) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +53 -38
  24. package/dist/cc.js.map +1 -1
  25. package/dist/config.js +6 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.js +14 -1
  28. package/dist/constants.js.map +1 -1
  29. package/dist/index.js +13 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/behavioral-events.js +26 -0
  32. package/dist/metrics/behavioral-events.js.map +1 -1
  33. package/dist/metrics/constants.js +9 -3
  34. package/dist/metrics/constants.js.map +1 -1
  35. package/dist/services/ApiAiAssistant.js +74 -3
  36. package/dist/services/ApiAiAssistant.js.map +1 -1
  37. package/dist/services/config/Util.js +2 -2
  38. package/dist/services/config/Util.js.map +1 -1
  39. package/dist/services/config/types.js +25 -9
  40. package/dist/services/config/types.js.map +1 -1
  41. package/dist/services/core/Utils.js +74 -27
  42. package/dist/services/core/Utils.js.map +1 -1
  43. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  44. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  45. package/dist/services/core/websocket/types.js.map +1 -1
  46. package/dist/services/index.js +1 -1
  47. package/dist/services/index.js.map +1 -1
  48. package/dist/services/task/Task.js +688 -0
  49. package/dist/services/task/Task.js.map +1 -0
  50. package/dist/services/task/TaskFactory.js +45 -0
  51. package/dist/services/task/TaskFactory.js.map +1 -0
  52. package/dist/services/task/TaskManager.js +725 -612
  53. package/dist/services/task/TaskManager.js.map +1 -1
  54. package/dist/services/task/TaskUtils.js +162 -26
  55. package/dist/services/task/TaskUtils.js.map +1 -1
  56. package/dist/services/task/constants.js +6 -1
  57. package/dist/services/task/constants.js.map +1 -1
  58. package/dist/services/task/digital/Digital.js +77 -0
  59. package/dist/services/task/digital/Digital.js.map +1 -0
  60. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  61. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  62. package/dist/services/task/state-machine/actions.js +543 -0
  63. package/dist/services/task/state-machine/actions.js.map +1 -0
  64. package/dist/services/task/state-machine/constants.js +161 -0
  65. package/dist/services/task/state-machine/constants.js.map +1 -0
  66. package/dist/services/task/state-machine/guards.js +340 -0
  67. package/dist/services/task/state-machine/guards.js.map +1 -0
  68. package/dist/services/task/state-machine/index.js +53 -0
  69. package/dist/services/task/state-machine/index.js.map +1 -0
  70. package/dist/services/task/state-machine/types.js +54 -0
  71. package/dist/services/task/state-machine/types.js.map +1 -0
  72. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  73. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  74. package/dist/services/task/taskDataNormalizer.js +99 -0
  75. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  76. package/dist/services/task/types.js +168 -16
  77. package/dist/services/task/types.js.map +1 -1
  78. package/dist/services/task/voice/Voice.js +1042 -0
  79. package/dist/services/task/voice/Voice.js.map +1 -0
  80. package/dist/services/task/voice/WebRTC.js +149 -0
  81. package/dist/services/task/voice/WebRTC.js.map +1 -0
  82. package/dist/types/cc.d.ts +19 -18
  83. package/dist/types/config.d.ts +6 -0
  84. package/dist/types/constants.d.ts +14 -1
  85. package/dist/types/index.d.ts +11 -5
  86. package/dist/types/metrics/constants.d.ts +7 -1
  87. package/dist/types/services/ApiAiAssistant.d.ts +11 -3
  88. package/dist/types/services/config/types.d.ts +113 -9
  89. package/dist/types/services/core/Utils.d.ts +23 -10
  90. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  91. package/dist/types/services/core/websocket/types.d.ts +1 -1
  92. package/dist/types/services/index.d.ts +1 -1
  93. package/dist/types/services/task/Task.d.ts +157 -0
  94. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  95. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  96. package/dist/types/services/task/constants.d.ts +5 -0
  97. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  98. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  99. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  100. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  101. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  102. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  103. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  104. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  105. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  106. package/dist/types/services/task/types.d.ts +551 -78
  107. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  108. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  109. package/dist/types/types.d.ts +92 -0
  110. package/dist/types/webex.d.ts +1 -0
  111. package/dist/types.js +85 -0
  112. package/dist/types.js.map +1 -1
  113. package/dist/webex.js +14 -2
  114. package/dist/webex.js.map +1 -1
  115. package/package.json +15 -12
  116. package/src/cc.ts +63 -43
  117. package/src/config.ts +6 -0
  118. package/src/constants.ts +14 -1
  119. package/src/index.ts +14 -5
  120. package/src/metrics/ai-docs/AGENTS.md +348 -0
  121. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  122. package/src/metrics/behavioral-events.ts +28 -0
  123. package/src/metrics/constants.ts +9 -4
  124. package/src/services/ApiAiAssistant.ts +104 -3
  125. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  126. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  127. package/src/services/ai-docs/AGENTS.md +384 -0
  128. package/src/services/config/Util.ts +2 -2
  129. package/src/services/config/ai-docs/AGENTS.md +253 -0
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  131. package/src/services/config/types.ts +116 -10
  132. package/src/services/core/Utils.ts +85 -34
  133. package/src/services/core/ai-docs/AGENTS.md +379 -0
  134. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  135. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  136. package/src/services/core/websocket/types.ts +1 -1
  137. package/src/services/index.ts +1 -1
  138. package/src/services/task/Task.ts +837 -0
  139. package/src/services/task/TaskFactory.ts +55 -0
  140. package/src/services/task/TaskManager.ts +738 -697
  141. package/src/services/task/TaskUtils.ts +205 -25
  142. package/src/services/task/ai-docs/AGENTS.md +455 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
  144. package/src/services/task/constants.ts +5 -0
  145. package/src/services/task/digital/Digital.ts +95 -0
  146. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  147. package/src/services/task/state-machine/actions.ts +685 -0
  148. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  149. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  150. package/src/services/task/state-machine/constants.ts +172 -0
  151. package/src/services/task/state-machine/guards.ts +406 -0
  152. package/src/services/task/state-machine/index.ts +28 -0
  153. package/src/services/task/state-machine/types.ts +241 -0
  154. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  155. package/src/services/task/taskDataNormalizer.ts +137 -0
  156. package/src/services/task/types.ts +654 -85
  157. package/src/services/task/voice/Voice.ts +1267 -0
  158. package/src/services/task/voice/WebRTC.ts +187 -0
  159. package/src/types.ts +112 -1
  160. package/src/utils/AGENTS.md +276 -0
  161. package/src/webex.js +2 -0
  162. package/test/unit/spec/cc.ts +133 -3
  163. package/test/unit/spec/logger-proxy.ts +70 -0
  164. package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
  165. package/test/unit/spec/services/WebCallingService.ts +7 -1
  166. package/test/unit/spec/services/config/index.ts +27 -27
  167. package/test/unit/spec/services/core/Utils.ts +335 -1
  168. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  169. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  170. package/test/unit/spec/services/task/Task.ts +477 -0
  171. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  172. package/test/unit/spec/services/task/TaskManager.ts +821 -1936
  173. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  174. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  175. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  176. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  177. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  178. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  179. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  180. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  181. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  182. package/umd/contact-center.min.js +2 -2
  183. package/umd/contact-center.min.js.map +1 -1
  184. package/dist/services/task/index.js +0 -1530
  185. package/dist/services/task/index.js.map +0 -1
  186. package/dist/types/services/task/index.d.ts +0 -650
  187. package/src/services/task/index.ts +0 -1806
  188. package/test/unit/spec/services/task/index.ts +0 -2205
@@ -0,0 +1,867 @@
1
+ /**
2
+ * UI Controls Computer - Centralized logic for computing UI control states
3
+ */
4
+
5
+ import {
6
+ InteractionUIControls,
7
+ TASK_CHANNEL_TYPE,
8
+ TaskData,
9
+ TaskUILeg,
10
+ TaskUIControls,
11
+ VOICE_VARIANT,
12
+ } from '../types';
13
+ import {TaskContext, UIControlConfig} from './types';
14
+ import {TaskState, MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE} from './constants';
15
+ import {
16
+ getIsCustomerInCall,
17
+ getConferenceParticipantsCount,
18
+ getIsConferenceInProgress,
19
+ getIsConsultInProgressForConferenceControls,
20
+ getIsConsultedAgentForControls,
21
+ getServerHoldStateForControls,
22
+ } from '../TaskUtils';
23
+
24
+ const DISABLED = {isVisible: false, isEnabled: false} as const;
25
+ const VISIBLE_ENABLED = {isVisible: true, isEnabled: true} as const;
26
+ const VISIBLE_DISABLED = {isVisible: true, isEnabled: false} as const;
27
+
28
+ function getDefaultInteractionUIControls(): InteractionUIControls {
29
+ return {
30
+ accept: DISABLED,
31
+ decline: DISABLED,
32
+ hold: DISABLED,
33
+ mute: DISABLED,
34
+ end: DISABLED,
35
+ transfer: DISABLED,
36
+ consult: DISABLED,
37
+ consultTransfer: DISABLED,
38
+ endConsult: DISABLED,
39
+ recording: DISABLED,
40
+ conference: DISABLED,
41
+ wrapup: DISABLED,
42
+ exitConference: DISABLED,
43
+ transferConference: DISABLED,
44
+ mergeToConference: DISABLED,
45
+ switch: DISABLED,
46
+ };
47
+ }
48
+
49
+ function createTaskUIControls(
50
+ main: InteractionUIControls,
51
+ consult: InteractionUIControls,
52
+ activeLeg: TaskUILeg
53
+ ): TaskUIControls {
54
+ return {
55
+ main,
56
+ consult,
57
+ activeLeg,
58
+ };
59
+ }
60
+
61
+ export function getDefaultUIControls(): TaskUIControls {
62
+ return createTaskUIControls(
63
+ getDefaultInteractionUIControls(),
64
+ getDefaultInteractionUIControls(),
65
+ 'main'
66
+ );
67
+ }
68
+
69
+ /** Consult media must exist on the interaction payload, not only as a stale resource id. */
70
+ function hasConsultMediaInInteraction(
71
+ interaction: TaskData['interaction'] | undefined,
72
+ consultMediaResourceId?: string
73
+ ): boolean {
74
+ const media = interaction?.media ?? {};
75
+ const hasConsultLeg = Object.values(media).some((entry: any) => entry?.mType === 'consult');
76
+
77
+ if (hasConsultLeg) return true;
78
+ if (!consultMediaResourceId) return false;
79
+
80
+ return Boolean(media[consultMediaResourceId]);
81
+ }
82
+
83
+ function computeVoiceInteractionUIControls(
84
+ state: TaskState,
85
+ context: TaskContext,
86
+ config: UIControlConfig,
87
+ fallbackTaskData?: TaskData,
88
+ currentLeg: TaskUILeg = 'main'
89
+ ): InteractionUIControls {
90
+ // Early exit for idle
91
+ if (state === TaskState.IDLE) {
92
+ return getDefaultInteractionUIControls();
93
+ }
94
+
95
+ // Prefer live task.data (fallback) over stale state-machine snapshot during multi-login sync.
96
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
97
+ const interaction = taskData?.interaction;
98
+ const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
99
+ const isWebrtc = config.voiceVariant === VOICE_VARIANT.WEBRTC;
100
+ const isOutdial = interaction?.outboundType === 'OUTDIAL';
101
+ const serverHold = getServerHoldStateForControls(context, mainCallId, fallbackTaskData);
102
+
103
+ // Backend-derived checks
104
+ const customerInCall =
105
+ interaction && mainCallId ? getIsCustomerInCall(interaction, mainCallId) : false;
106
+ // EP-DN/secondary legs can have incomplete media participant lists; fall back to participants map.
107
+ const customerPresent =
108
+ customerInCall ||
109
+ Boolean(
110
+ interaction &&
111
+ interaction.participants &&
112
+ Object.values(interaction.participants).some(
113
+ (p: any) => p?.pType === 'Customer' && !p?.hasLeft
114
+ )
115
+ );
116
+ const participantCount =
117
+ interaction && mainCallId ? getConferenceParticipantsCount(interaction, mainCallId) : 0;
118
+ const maxParticipants = participantCount >= MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE;
119
+ const selfAgentId = config.agentId ?? taskData?.agentId;
120
+ const consultInProgress = getIsConsultInProgressForConferenceControls(
121
+ interaction,
122
+ mainCallId,
123
+ selfAgentId
124
+ );
125
+ const conferenceFromBackend = taskData ? getIsConferenceInProgress(taskData) : false;
126
+ // Note: ownership is used by some controls; keep computations local to those controls
127
+
128
+ // Context flags (set by state machine actions)
129
+ const {
130
+ consultInitiator,
131
+ consultDestinationAgentJoined,
132
+ consultDestinationType,
133
+ consultCallHeld,
134
+ consultFromConference,
135
+ } = context;
136
+ const {recordingControlsAvailable} = context;
137
+
138
+ // EP_DN consults are "ready" as soon as the consult is created (EP accepts routing immediately).
139
+ // Backend sends destinationType as 'EP-DN'; SDK method uses 'entryPoint' — check both.
140
+ const isEpDnConsult =
141
+ consultDestinationType === 'entryPoint' || consultDestinationType === ('EP-DN' as any);
142
+
143
+ const stateImpliesHeld = state === TaskState.HELD || state === TaskState.RESUME_INITIATING;
144
+ const stateImpliesConnected =
145
+ state === TaskState.CONNECTED || state === TaskState.HOLD_INITIATING;
146
+ const isHeld = stateImpliesHeld || serverHold === true;
147
+ const isConnected = stateImpliesConnected || (!stateImpliesHeld && serverHold === false);
148
+
149
+ // State categories for cleaner logic
150
+ const isConsulting =
151
+ state === TaskState.CONSULTING ||
152
+ state === TaskState.CONSULT_INITIATING ||
153
+ state === TaskState.CONF_INITIATING;
154
+ const isConferencing = state === TaskState.CONFERENCING;
155
+ const isWrappingUp = state === TaskState.WRAPPING_UP;
156
+ const selfInMainCall =
157
+ Boolean(selfAgentId) &&
158
+ Boolean(mainCallId) &&
159
+ Boolean(interaction?.media?.[mainCallId]?.participants?.includes(selfAgentId as string));
160
+ const consultMedia = Object.values(interaction?.media ?? {}).find(
161
+ (media: any) =>
162
+ media?.mediaResourceId === taskData?.consultMediaResourceId || media?.mType === 'consult'
163
+ ) as {participants?: string[]} | undefined;
164
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
165
+ const selfInConsultCall =
166
+ Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
167
+ const hasConsultMedia = hasConsultMediaInInteraction(
168
+ interaction,
169
+ taskData?.consultMediaResourceId
170
+ );
171
+ const isConsultEndedForSelf =
172
+ taskData?.type === 'AgentConsultEnded' ||
173
+ taskData?.type === 'AgentConsultFailed' ||
174
+ (selfParticipant?.consultState === 'consultCompleted' &&
175
+ !hasConsultMedia &&
176
+ taskData?.isConsulted === false);
177
+ const effectiveConsultInitiator = isConsultEndedForSelf ? false : consultInitiator;
178
+ const effectiveConsultCallHeld = isConsultEndedForSelf ? false : consultCallHeld;
179
+ const effectiveConsultFromConference = isConsultEndedForSelf ? false : consultFromConference;
180
+ const effectiveConsultDestinationAgentJoined = isConsultEndedForSelf
181
+ ? false
182
+ : consultDestinationAgentJoined;
183
+ // After a consult ends for self, the backend stops sending consult media but the merged
184
+ // task.data can still carry the stale consult-media entry (reconcileData never deletes keys).
185
+ // Treat it as gone so post-consult main-leg controls (consult retry) are not blocked.
186
+ const effectiveHasConsultMedia = isConsultEndedForSelf ? false : hasConsultMedia;
187
+ const isConsultDestinationReady = effectiveConsultDestinationAgentJoined || isEpDnConsult;
188
+ const conferenceActive =
189
+ isConferencing || conferenceFromBackend || effectiveConsultFromConference;
190
+ // Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
191
+ const inConference =
192
+ conferenceActive && (isConferencing || selfInMainCall || effectiveConsultInitiator);
193
+
194
+ // Check if this is a consulted agent (must be after isConsulting is computed).
195
+ const isSoleAgentOnCall =
196
+ participantCount <= 1 && selfInMainCall && !isConsulting && !inConference;
197
+ const isConsulted =
198
+ inConference || isSoleAgentOnCall
199
+ ? false
200
+ : getIsConsultedAgentForControls(taskData, context, isConsulting) ||
201
+ (!effectiveConsultInitiator &&
202
+ (selfParticipant?.isConsulted === true ||
203
+ selfParticipant?.consultState === 'consulting'));
204
+
205
+ // Active call = can perform call operations
206
+ const isActive =
207
+ state === TaskState.CONNECTED ||
208
+ state === TaskState.HELD ||
209
+ state === TaskState.HOLD_INITIATING ||
210
+ state === TaskState.RESUME_INITIATING ||
211
+ isConsulting ||
212
+ isConferencing;
213
+
214
+ // Consulted agents have limited controls until they're in conference or wrapup
215
+ // Use inConference (not isConferencing) so controls remain enabled after state downgrade
216
+ const hasFullControls = !isConsulted || effectiveConsultInitiator || inConference || isWrappingUp;
217
+ const consultOwnedBySelf =
218
+ effectiveConsultInitiator ||
219
+ (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
220
+ const selfConsultPendingOnConsultMedia =
221
+ selfParticipant?.consultState === 'consultInitiated' &&
222
+ !taskData?.isConsulted &&
223
+ hasConsultMedia;
224
+ const ownerParticipant = interaction?.owner
225
+ ? interaction.participants?.[interaction.owner]
226
+ : undefined;
227
+ const otherAgentConsultInProgress = Boolean(
228
+ interaction?.participants &&
229
+ Object.values(interaction.participants).some((participant: any) => {
230
+ if (!participant || participant.hasLeft) return false;
231
+ if (participant.id === selfAgentId) return false;
232
+ if (participant.pType !== 'AGENT') return false;
233
+
234
+ return (
235
+ participant.consultState === 'consultInitiated' ||
236
+ participant.consultState === 'consultReserved' ||
237
+ participant.consultState === 'consulting' ||
238
+ participant.currentState === 'consulting'
239
+ );
240
+ })
241
+ );
242
+ const isHydratedConferenceConsultPending =
243
+ inConference && selfConsultPendingOnConsultMedia && !effectiveConsultDestinationAgentJoined;
244
+ const hasParallelConsultLeg =
245
+ !isConsultEndedForSelf &&
246
+ consultOwnedBySelf &&
247
+ !isConsulting &&
248
+ !isConsulted &&
249
+ (consultInProgress || effectiveConsultCallHeld || hasConsultMedia);
250
+ const activeLegForConferenceConsult = effectiveConsultCallHeld ? 'main' : 'consult';
251
+ const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
252
+ const isConferenceConsultTransferContext =
253
+ inConference && effectiveConsultInitiator && hasConsultMedia && isConsultDestinationReady;
254
+ const consultLegOnHold = isConsulting && effectiveConsultCallHeld;
255
+ const callProcessingDetails = interaction?.callProcessingDetails as
256
+ | {conferenceHoldParticipant?: boolean | string}
257
+ | undefined;
258
+ const conferenceHoldParticipant =
259
+ callProcessingDetails?.conferenceHoldParticipant === true ||
260
+ callProcessingDetails?.conferenceHoldParticipant === 'true';
261
+ const postDeclineHeldMainLeg =
262
+ consultInitiator &&
263
+ !consultDestinationAgentJoined &&
264
+ isHeld &&
265
+ inConference &&
266
+ conferenceHoldParticipant;
267
+ const postConsultCompletedHeldMainLeg =
268
+ selfParticipant?.consultState === 'consultCompleted' && isHeld && inConference && !isConsulting;
269
+ const nonOwnerPostConsultCompletedHeldMainLeg =
270
+ isHeld &&
271
+ inConference &&
272
+ !isConsulting &&
273
+ !consultInitiator &&
274
+ Boolean(selfAgentId) &&
275
+ Boolean(interaction?.owner) &&
276
+ selfAgentId !== interaction?.owner &&
277
+ ownerParticipant?.consultState === 'consultCompleted';
278
+ const isConsultPendingBeforeJoin =
279
+ selfParticipant?.consultState === 'consultInitiated' && !consultDestinationAgentJoined;
280
+ const hideExitConferenceWhileConsultPending =
281
+ currentLeg === 'main' &&
282
+ inConference &&
283
+ isConsultPendingBeforeJoin &&
284
+ (consultFromConference ||
285
+ consultInitiator ||
286
+ taskData?.type === 'AgentConsultCreated' ||
287
+ consultInProgress ||
288
+ isConsulting);
289
+ const hideExitConferenceDuringActiveConsultFromConference =
290
+ inConference &&
291
+ consultInitiator &&
292
+ consultDestinationAgentJoined &&
293
+ (isConsulting ||
294
+ taskData?.type === 'AgentConsulting' ||
295
+ selfParticipant?.consultState === 'consulting');
296
+ const hideExitConferenceOnMainLegForEpDnConsultFromConference =
297
+ currentLeg === 'main' &&
298
+ inConference &&
299
+ consultFromConference &&
300
+ consultInitiator &&
301
+ (isConsulting ||
302
+ consultInProgress ||
303
+ taskData?.type === 'AgentConsultCreated' ||
304
+ taskData?.type === 'AgentConsulting' ||
305
+ selfParticipant?.consultState === 'consultInitiated' ||
306
+ selfParticipant?.consultState === 'consulting');
307
+ const forceHeldPostConsultControls =
308
+ !hideExitConferenceWhileConsultPending &&
309
+ (postDeclineHeldMainLeg || postConsultCompletedHeldMainLeg);
310
+ const selfOnConsultLeg =
311
+ selfParticipant?.consultState === 'consulting' ||
312
+ selfParticipant?.currentState === 'consulting';
313
+ const showMainLegConferenceControlsDuringConsult =
314
+ currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
315
+ const allowHeldMainLegControlsForNonInitiator =
316
+ showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
317
+ const isConsultRequestedPhase =
318
+ isConsultPendingBeforeJoin &&
319
+ !isConsulted &&
320
+ (consultInitiator ||
321
+ (Boolean(selfAgentId) &&
322
+ selfParticipant?.consultState === 'consultInitiated' &&
323
+ taskData?.agentId === selfAgentId));
324
+ const isConsultUnansweredFailure =
325
+ currentLeg === 'main' &&
326
+ !inConference &&
327
+ !taskData?.isConsulted &&
328
+ !effectiveHasConsultMedia &&
329
+ isHeld &&
330
+ selfParticipant?.consultState === 'consultCompleted' &&
331
+ (taskData?.type === 'AgentConsultFailed' ||
332
+ taskData?.type === 'AgentConsultEnded' ||
333
+ isConsultEndedForSelf);
334
+
335
+ if (isConsultUnansweredFailure) {
336
+ const recordingControl =
337
+ recordingControlsAvailable && config.isRecordingEnabled && !inConference
338
+ ? VISIBLE_ENABLED
339
+ : DISABLED;
340
+
341
+ return {
342
+ ...getDefaultInteractionUIControls(),
343
+ hold: VISIBLE_ENABLED,
344
+ transfer: VISIBLE_ENABLED,
345
+ consult: VISIBLE_ENABLED,
346
+ recording: recordingControl,
347
+ end: VISIBLE_DISABLED,
348
+ };
349
+ }
350
+
351
+ if (isConsultRequestedPhase) {
352
+ if (currentLeg === 'main') {
353
+ return {
354
+ ...getDefaultInteractionUIControls(),
355
+ transfer: VISIBLE_DISABLED,
356
+ conference: VISIBLE_DISABLED,
357
+ end: VISIBLE_DISABLED,
358
+ };
359
+ }
360
+
361
+ if (currentLeg === 'consult') {
362
+ return {
363
+ ...getDefaultInteractionUIControls(),
364
+ endConsult: VISIBLE_ENABLED,
365
+ switch: VISIBLE_DISABLED,
366
+ transfer: VISIBLE_DISABLED,
367
+ transferConference: inConference ? VISIBLE_DISABLED : DISABLED,
368
+ mergeToConference: VISIBLE_DISABLED,
369
+ };
370
+ }
371
+ }
372
+
373
+ return {
374
+ // Accept/Decline: Voice tasks in offered state
375
+ // Desktop/WebRTC + inbound: accept enabled (agent manually accepts)
376
+ // Desktop/WebRTC + outdial: accept disabled (auto-answer handles it; Widgets show "Accept" disabled)
377
+ // Extension mode (non-WebRTC): accept disabled (Widgets show "Ringing...")
378
+ accept:
379
+ state === TaskState.OFFERED && !interaction?.isTerminated
380
+ ? {isVisible: true, isEnabled: isWebrtc && !isOutdial}
381
+ : DISABLED,
382
+ decline: (() => {
383
+ if (!isWebrtc || state !== TaskState.OFFERED || interaction?.isTerminated) return DISABLED;
384
+
385
+ return isOutdial ? VISIBLE_DISABLED : VISIBLE_ENABLED;
386
+ })(),
387
+
388
+ // Hold: visible in connected/held/conference, disabled in conference/consulting
389
+ hold: (() => {
390
+ if (!hasFullControls) return DISABLED;
391
+ if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
392
+ if (
393
+ consultOwnedBySelf &&
394
+ (isConsulting || hasParallelConsultLeg || effectiveConsultCallHeld)
395
+ ) {
396
+ return DISABLED;
397
+ }
398
+ if (hasParallelConsultLeg) return DISABLED;
399
+ if (state === TaskState.OFFERED) return DISABLED;
400
+ if (isWrappingUp) return DISABLED;
401
+ // Visibility: connected || held || inConference
402
+ if (!(isConnected || isHeld || inConference)) return DISABLED;
403
+ // Enabled: (connected || held) && !inConference && !isConsulting
404
+ const canHold = (isConnected || isHeld) && !inConference && !isConsulting;
405
+
406
+ return canHold ? VISIBLE_ENABLED : VISIBLE_DISABLED;
407
+ })(),
408
+
409
+ // Mute: WebRTC only, active calls; hidden entirely during wrapup
410
+ mute: (() => {
411
+ if (!isWebrtc) return DISABLED;
412
+ if (isWrappingUp) return DISABLED;
413
+ if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
414
+ // The consulted agent has no separate consult leg; their main leg is the active call, so
415
+ // mute stays available while they consult (the heuristic below targets the initiator's
416
+ // inactive leg, not the consultee's only leg).
417
+ const isConsultedActiveMainLeg = isConsulted && currentLeg === 'main';
418
+ if (
419
+ (isConsulting || hasParallelConsultLeg) &&
420
+ !isCurrentLegActive &&
421
+ !isConsultedActiveMainLeg
422
+ ) {
423
+ return VISIBLE_DISABLED;
424
+ }
425
+ if (isConsulting) return VISIBLE_ENABLED;
426
+
427
+ if (isConnected || isHeld || isConferencing) {
428
+ if (inConference) return VISIBLE_ENABLED;
429
+
430
+ return isHeld ? VISIBLE_DISABLED : VISIBLE_ENABLED;
431
+ }
432
+
433
+ return DISABLED;
434
+ })(),
435
+
436
+ // End: varies by state; during consulting only on main leg (consult held)
437
+ end: (() => {
438
+ if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
439
+ if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
440
+ if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
441
+ if (!config.isEndTaskEnabled && !isWebrtc) return DISABLED;
442
+ if (hasParallelConsultLeg) {
443
+ return isConnected && isEpDnConsult ? VISIBLE_ENABLED : VISIBLE_DISABLED;
444
+ }
445
+
446
+ if (isConsulting) {
447
+ if (currentLeg === 'consult' && consultCallHeld) return DISABLED;
448
+
449
+ return consultInitiator && consultCallHeld ? VISIBLE_ENABLED : DISABLED;
450
+ }
451
+
452
+ if (inConference) {
453
+ if (isConsulted) return DISABLED;
454
+ if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
455
+
456
+ if (consultInProgress) return VISIBLE_DISABLED;
457
+
458
+ return isWrappingUp ? VISIBLE_DISABLED : VISIBLE_ENABLED;
459
+ }
460
+ if (!hasFullControls) return DISABLED;
461
+ if (isActive) return isHeld || isWrappingUp ? VISIBLE_DISABLED : VISIBLE_ENABLED;
462
+
463
+ return DISABLED;
464
+ })(),
465
+
466
+ // Transfer: connected/held/conference
467
+ transfer: (() => {
468
+ if (isHydratedConferenceConsultPending) return VISIBLE_DISABLED;
469
+ if (isConferenceConsultTransferContext && currentLeg === 'main' && isCurrentLegActive) {
470
+ return DISABLED;
471
+ }
472
+ if (inConference && isConsulting && consultInitiator) return DISABLED;
473
+ if (hasParallelConsultLeg) {
474
+ if (!customerPresent) return DISABLED;
475
+ if (state === TaskState.CONNECTED) return VISIBLE_ENABLED;
476
+ if (state === TaskState.HELD) return VISIBLE_DISABLED;
477
+ }
478
+ if (isConsulting) {
479
+ if (!consultInitiator) return DISABLED;
480
+ if (!customerPresent) return VISIBLE_DISABLED;
481
+ if (consultLegOnHold) return VISIBLE_DISABLED;
482
+
483
+ return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
484
+ }
485
+ if (!hasFullControls) return DISABLED;
486
+ if (inConference) {
487
+ // Real conference (multiple agents): transfer is hidden
488
+ // Pending conference (only self agent): transfer remains available
489
+ return participantCount > 1 ? DISABLED : VISIBLE_ENABLED;
490
+ }
491
+ if (state === TaskState.CONNECTED || state === TaskState.HELD) return VISIBLE_ENABLED;
492
+
493
+ return DISABLED;
494
+ })(),
495
+
496
+ // Consult: connected/held/conference when conditions met
497
+ consult: (() => {
498
+ const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
499
+
500
+ if (
501
+ isHeld &&
502
+ !inConference &&
503
+ !effectiveHasConsultMedia &&
504
+ selfParticipant?.consultState === 'consultCompleted' &&
505
+ !taskData?.isConsulted
506
+ ) {
507
+ const canRetryConsult = !maxParticipants && customerPresent && !otherAgentConsultInProgress;
508
+
509
+ return {isVisible: true, isEnabled: canRetryConsult};
510
+ }
511
+
512
+ if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
513
+ if (hasParallelConsultLeg) return DISABLED;
514
+ if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
515
+ return DISABLED;
516
+ }
517
+
518
+ // In conference: behavior depends on whether it's a real multi-agent conference
519
+ if (inConference) {
520
+ // Pending conference (only self agent): consult disabled
521
+ if (participantCount <= 1) return VISIBLE_DISABLED;
522
+ // Real conference: consult enabled if conditions met
523
+ const canFromConference =
524
+ !maxParticipants &&
525
+ customerPresent &&
526
+ !consultInProgress &&
527
+ !otherAgentConsultInProgress &&
528
+ !isConsulting;
529
+
530
+ return {isVisible: true, isEnabled: canFromConference};
531
+ }
532
+
533
+ // Enabled conditions for connected/held
534
+ const canFromConnected =
535
+ !maxParticipants && customerPresent && !consultInProgress && !isConsulted;
536
+
537
+ return {isVisible: true, isEnabled: canFromConnected};
538
+ })(),
539
+
540
+ // ConsultTransfer: always hidden (use transfer button)
541
+ consultTransfer: DISABLED,
542
+
543
+ // EndConsult: during consulting
544
+ endConsult: (() => {
545
+ if (!isConsulting) return DISABLED;
546
+ if (isConsulted && isConferencing) return DISABLED;
547
+ if (!isConsulted && isConferencing && !(consultInitiator && conferenceFromBackend)) {
548
+ return DISABLED;
549
+ }
550
+
551
+ return {isVisible: true, isEnabled: consultInitiator || config.isEndConsultEnabled};
552
+ })(),
553
+
554
+ // Recording: connected/held, hidden in real conference, visible in pending conference
555
+ recording: (() => {
556
+ if (!recordingControlsAvailable || !config.isRecordingEnabled) return DISABLED;
557
+ if (!hasFullControls || isConsulting) return DISABLED;
558
+ if (inConference) {
559
+ // Real conference (multiple agents): recording hidden
560
+ // Pending conference (only self agent): recording available
561
+ return participantCount > 1 ? DISABLED : VISIBLE_ENABLED;
562
+ }
563
+ if (hasParallelConsultLeg && !customerPresent) return DISABLED;
564
+ if (state === TaskState.CONNECTED || state === TaskState.HELD) {
565
+ return VISIBLE_ENABLED;
566
+ }
567
+
568
+ return DISABLED;
569
+ })(),
570
+
571
+ // Conference: during consulting, enabled on both legs when agent joined
572
+ // Label changes based on leg: "Conference" on main leg, "Merge" on consult leg
573
+ conference: (() => {
574
+ if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
575
+ if (hasParallelConsultLeg) {
576
+ if (!customerPresent) return DISABLED;
577
+ if (state === TaskState.CONNECTED) {
578
+ return maxParticipants ? VISIBLE_DISABLED : VISIBLE_ENABLED;
579
+ }
580
+ if (state === TaskState.HELD) return VISIBLE_DISABLED;
581
+
582
+ return DISABLED;
583
+ }
584
+ if (!hasFullControls || !isConsulting) return DISABLED;
585
+ if (!consultInitiator) return DISABLED;
586
+ if (!customerPresent) return VISIBLE_DISABLED;
587
+ if (consultLegOnHold) return VISIBLE_DISABLED;
588
+
589
+ return isConsultDestinationReady && !maxParticipants ? VISIBLE_ENABLED : VISIBLE_DISABLED;
590
+ })(),
591
+
592
+ // Wrapup: wrapping up state
593
+ wrapup: isWrappingUp ? VISIBLE_ENABLED : DISABLED,
594
+
595
+ // ExitConference: in conference with multiple agents in main call
596
+ exitConference: (() => {
597
+ if (hideExitConferenceWhileConsultPending) return DISABLED;
598
+ if (hideExitConferenceOnMainLegForEpDnConsultFromConference) return DISABLED;
599
+ if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
600
+ if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
601
+ if (hideExitConferenceDuringActiveConsultFromConference) return DISABLED;
602
+ if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
603
+ if (isConsulted && !isConferencing) return DISABLED;
604
+ if (!inConference) return DISABLED;
605
+ if (participantCount <= 1) return DISABLED;
606
+ if (consultInProgress) return VISIBLE_DISABLED;
607
+ const consultingFromConference = consultInitiator && isConsulting && conferenceFromBackend;
608
+
609
+ return consultingFromConference ? VISIBLE_DISABLED : VISIBLE_ENABLED;
610
+ })(),
611
+
612
+ // TransferConference: in conference with active consult, owner consulting from conference
613
+ transferConference: (() => {
614
+ if (isConferenceConsultTransferContext && !isCurrentLegActive) return VISIBLE_DISABLED;
615
+ const consultLegTransferAvailable =
616
+ currentLeg === 'consult' && inConference && consultInitiator && hasConsultMedia;
617
+ const selfConsultingOnParticipantState = selfParticipant?.consultState === 'consulting';
618
+ const conferenceTransferAvailable =
619
+ consultLegTransferAvailable ||
620
+ (inConference &&
621
+ consultInitiator &&
622
+ hasConsultMedia &&
623
+ (isConsulting || consultInProgress || selfConsultingOnParticipantState));
624
+ if (consultLegOnHold) return DISABLED;
625
+ if (hasParallelConsultLeg && !conferenceTransferAvailable) return DISABLED;
626
+ if (!conferenceTransferAvailable && (!inConference || !isConsulting)) return DISABLED;
627
+ if (!consultInitiator || isConsulted) return DISABLED;
628
+
629
+ return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
630
+ })(),
631
+
632
+ // MergeToConference: mirrors conference control, enabled on both legs
633
+ mergeToConference: (() => {
634
+ if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
635
+ if (!isConsulting || !consultInitiator) return DISABLED;
636
+ if (!customerPresent) return VISIBLE_DISABLED;
637
+ if (consultLegOnHold) return VISIBLE_DISABLED;
638
+
639
+ return isConsultDestinationReady && !maxParticipants ? VISIBLE_ENABLED : VISIBLE_DISABLED;
640
+ })(),
641
+
642
+ // Switch: visible only on the currently active leg
643
+ switch: (() => {
644
+ if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
645
+ if (!customerPresent && hasParallelConsultLeg) return DISABLED;
646
+ if (currentLeg === 'consult') {
647
+ if (!isConsulting || !consultInitiator || consultCallHeld) return DISABLED;
648
+ if (!customerPresent) return VISIBLE_DISABLED;
649
+
650
+ return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
651
+ }
652
+
653
+ if (hasParallelConsultLeg && state === TaskState.CONNECTED) {
654
+ return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
655
+ }
656
+
657
+ return DISABLED;
658
+ })(),
659
+ };
660
+ }
661
+
662
+ function computeDigitalInteractionUIControls(
663
+ state: TaskState,
664
+ context: TaskContext,
665
+ fallbackTaskData?: TaskData
666
+ ): InteractionUIControls {
667
+ const taskData = context.taskData ?? fallbackTaskData ?? null;
668
+ const isTerminated = taskData?.interaction?.isTerminated ?? false;
669
+
670
+ const isConnected = state === TaskState.CONNECTED;
671
+ const isWrappingUp = state === TaskState.WRAPPING_UP;
672
+
673
+ return {
674
+ accept: state === TaskState.OFFERED ? VISIBLE_ENABLED : DISABLED,
675
+ decline: DISABLED,
676
+ hold: DISABLED,
677
+ mute: DISABLED,
678
+ end: isConnected && !isWrappingUp ? VISIBLE_ENABLED : DISABLED,
679
+ transfer: isConnected && !isWrappingUp ? VISIBLE_ENABLED : DISABLED,
680
+ consult: DISABLED,
681
+ consultTransfer: DISABLED,
682
+ endConsult: DISABLED,
683
+ recording: DISABLED,
684
+ conference: DISABLED,
685
+ wrapup: isTerminated || isWrappingUp ? VISIBLE_ENABLED : DISABLED,
686
+ exitConference: DISABLED,
687
+ transferConference: DISABLED,
688
+ mergeToConference: DISABLED,
689
+ switch: DISABLED,
690
+ };
691
+ }
692
+
693
+ function getVoiceLegState(
694
+ currentState: TaskState,
695
+ context: TaskContext,
696
+ config: UIControlConfig,
697
+ fallbackTaskData?: TaskData
698
+ ): {hasConsultLeg: boolean; activeLeg: TaskUILeg; mainState: TaskState; consultState: TaskState} {
699
+ if (currentState === TaskState.WRAPPING_UP) {
700
+ return {
701
+ hasConsultLeg: false,
702
+ activeLeg: 'main',
703
+ mainState: currentState,
704
+ consultState: TaskState.CONSULTING,
705
+ };
706
+ }
707
+
708
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
709
+ const interaction = taskData?.interaction;
710
+ const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
711
+ const selfAgentId = config.agentId ?? taskData?.agentId;
712
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
713
+ const hasConsultMedia = hasConsultMediaInInteraction(
714
+ interaction,
715
+ taskData?.consultMediaResourceId
716
+ );
717
+ const isConsultEndedForSelf =
718
+ taskData?.type === 'AgentConsultEnded' ||
719
+ taskData?.type === 'AgentConsultFailed' ||
720
+ (selfParticipant?.consultState === 'consultCompleted' &&
721
+ !hasConsultMedia &&
722
+ taskData?.isConsulted === false);
723
+ const consultInProgress = getIsConsultInProgressForConferenceControls(
724
+ interaction,
725
+ mainCallId,
726
+ selfAgentId
727
+ );
728
+ const isConsultingState =
729
+ currentState === TaskState.CONSULTING ||
730
+ currentState === TaskState.CONSULT_INITIATING ||
731
+ currentState === TaskState.CONF_INITIATING;
732
+ const consultOwnedBySelf =
733
+ context.consultInitiator ||
734
+ (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
735
+ const selfConsultPendingOnConsultMedia =
736
+ selfParticipant?.consultState === 'consultInitiated' &&
737
+ !taskData?.isConsulted &&
738
+ hasConsultMedia;
739
+ const selfConsultingOnConsultMedia =
740
+ selfParticipant?.consultState === 'consulting' && hasConsultMedia;
741
+ const isConsultUnansweredFailure =
742
+ !taskData?.isConsulted &&
743
+ !hasConsultMedia &&
744
+ selfParticipant?.consultState === 'consultCompleted' &&
745
+ Boolean(mainCallId && taskData?.interaction?.media?.[mainCallId]?.isHold === true) &&
746
+ taskData?.interaction?.state !== 'conference' &&
747
+ !getIsConferenceInProgress(taskData);
748
+ const hasConsultLeg = Boolean(
749
+ !isConsultEndedForSelf &&
750
+ !interaction?.isTerminated &&
751
+ !isConsultUnansweredFailure &&
752
+ (consultOwnedBySelf || selfConsultingOnConsultMedia || selfConsultPendingOnConsultMedia) &&
753
+ (consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
754
+ );
755
+
756
+ if (!hasConsultLeg) {
757
+ let mainState = currentState;
758
+ if (isConsultEndedForSelf && taskData?.interaction) {
759
+ const resolvedMainId = taskData.interaction.mainInteractionId || taskData.interactionId;
760
+ const isMainHeld = Boolean(
761
+ resolvedMainId && taskData.interaction.media?.[resolvedMainId]?.isHold === true
762
+ );
763
+
764
+ if (taskData.interaction.state === 'conference' || getIsConferenceInProgress(taskData)) {
765
+ mainState = TaskState.CONFERENCING;
766
+ } else if (isMainHeld || taskData.interaction.state === 'hold') {
767
+ mainState = TaskState.HELD;
768
+ } else {
769
+ mainState = TaskState.CONNECTED;
770
+ }
771
+ }
772
+
773
+ return {
774
+ hasConsultLeg: false,
775
+ activeLeg: 'main',
776
+ mainState,
777
+ consultState: TaskState.CONSULTING,
778
+ };
779
+ }
780
+
781
+ let mainState = TaskState.HELD;
782
+ if (currentState === TaskState.CONFERENCING) {
783
+ mainState = TaskState.CONFERENCING;
784
+ } else if (context.consultCallHeld) {
785
+ mainState = TaskState.CONNECTED;
786
+ }
787
+
788
+ return {
789
+ hasConsultLeg: true,
790
+ activeLeg: context.consultCallHeld ? 'main' : 'consult',
791
+ mainState,
792
+ consultState: isConsultingState ? currentState : TaskState.CONSULTING,
793
+ };
794
+ }
795
+
796
+ export function computeUIControls(
797
+ currentState: TaskState,
798
+ context: TaskContext,
799
+ fallbackTaskData?: TaskData
800
+ ): TaskUIControls {
801
+ if (currentState === TaskState.TERMINATED || currentState === TaskState.COMPLETED) {
802
+ return getDefaultUIControls();
803
+ }
804
+
805
+ switch (context.uiControlConfig.channelType) {
806
+ case TASK_CHANNEL_TYPE.VOICE: {
807
+ const {hasConsultLeg, activeLeg, mainState, consultState} = getVoiceLegState(
808
+ currentState,
809
+ context,
810
+ context.uiControlConfig,
811
+ fallbackTaskData
812
+ );
813
+
814
+ const mainControls = computeVoiceInteractionUIControls(
815
+ mainState,
816
+ context,
817
+ context.uiControlConfig,
818
+ fallbackTaskData,
819
+ 'main'
820
+ );
821
+ const consultControls = hasConsultLeg
822
+ ? computeVoiceInteractionUIControls(
823
+ consultState,
824
+ context,
825
+ context.uiControlConfig,
826
+ fallbackTaskData,
827
+ 'consult'
828
+ )
829
+ : getDefaultInteractionUIControls();
830
+
831
+ return createTaskUIControls(mainControls, consultControls, activeLeg);
832
+ }
833
+ case TASK_CHANNEL_TYPE.DIGITAL:
834
+ return createTaskUIControls(
835
+ computeDigitalInteractionUIControls(currentState, context, fallbackTaskData),
836
+ getDefaultInteractionUIControls(),
837
+ 'main'
838
+ );
839
+ default:
840
+ return getDefaultUIControls();
841
+ }
842
+ }
843
+
844
+ function haveInteractionUIControlsChanged(
845
+ previous: InteractionUIControls,
846
+ next: InteractionUIControls
847
+ ): boolean {
848
+ return (Object.keys(next) as (keyof InteractionUIControls)[]).some((key) => {
849
+ const prev = previous[key];
850
+ const curr = next[key];
851
+
852
+ return prev.isVisible !== curr.isVisible || prev.isEnabled !== curr.isEnabled;
853
+ });
854
+ }
855
+
856
+ export function haveUIControlsChanged(
857
+ previous: TaskUIControls | undefined,
858
+ next: TaskUIControls
859
+ ): boolean {
860
+ if (!previous) return true;
861
+
862
+ return (
863
+ previous.activeLeg !== next.activeLeg ||
864
+ haveInteractionUIControlsChanged(previous.main, next.main) ||
865
+ haveInteractionUIControlsChanged(previous.consult, next.consult)
866
+ );
867
+ }