feat(contact-center): add agent wellness breaks (WXCC-12423) - #741
feat(contact-center): add agent wellness breaks (WXCC-12423)#741bhabalan wants to merge 5 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bf2b5296d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? await import('./assets/WellnessBreakAnimationDark.json') | ||
| : await import('./assets/WellnessBreakAnimationLight.json'); |
There was a problem hiding this comment.
Commit the imported wellness animations
Both literal dynamic imports reference files that are absent from the commit: a repo-wide git ls-tree -r 2feb787 search finds neither WellnessBreakAnimationDark.json nor WellnessBreakAnimationLight.json (and .gitignore currently ignores *.json). Webpack must resolve literal dynamic imports while building, so the AI Assistant and aggregate widget builds fail before the runtime catch can provide the intended media fallback; force-add the two approved JSON assets or remove/replace these imports.
Useful? React with 👍 / 👎.
| /** Returns the system-owned `WellbeingBreak` idle code for the active registration. */ | ||
| getWellbeingBreakIdleCode(): Promise<IdleCode>; | ||
| /** Changes one or more Agent State Control channels and resolves from the matching SDK event. */ | ||
| setAgentChannelState(data: SetAgentChannelStateParams): Promise<AgentChannelStateChangedEvent>; |
There was a problem hiding this comment.
Upgrade the SDK before calling wellness APIs
This commit leaves @webex/contact-center pinned to the pre-existing 3.12.0-next.109 build while locally extending IContactCenter with wellness APIs; the added feature intake explicitly identifies these declarations as temporary and requires replacing the SDK dependency and using its package-root types before release. Until that dependency is supplied, the type checker cannot verify these methods/events against the runtime SDK, and a missing getWellbeingBreakIdleCode disables wellness while a missing state/action API makes accepted breaks fail. Upgrade to the WXCC-12423 SDK surface rather than asserting it locally.
AGENTS.md reference: AGENTS.md:L56-L57
Useful? React with 👍 / 👎.
| ccSDK.off(CC_EVENTS.WELLNESS_BREAK, this.handleWellnessBreak); | ||
| ccSDK.off(CC_EVENTS.AI_ASSISTANT_RTD_STATUS_CHANGED, this.handleAIAssistantRtdStatus); | ||
| ccSDK.off(CC_EVENTS.AGENT_CHANNEL_RELOGIN_SUCCESS, this.handleAgentChannelRelogin); | ||
| ccSDK.off(CC_EVENTS.AGENT_CHANNEL_STATE_CHANGED, this.handleAgentChannelStateChanged); |
There was a problem hiding this comment.
Re-add wellness listeners after logout
After a normal logout, handleLogOut invokes removeEventListeners, and these lines detach all four registration-level wellness/ASC callbacks. On the next station login or DN registration in the same page, addEventListeners only restores the task and legacy-state callbacks; the wellness on(...) calls at the end of setupIncomingTaskHandler are not executed again. Consequently subsequent login sessions stop receiving wellness offers, RTD status, and channel-state updates until the store is initialized again.
Useful? React with 👍 / 👎.
| const legacyRecoveryCountRef = useRef(0); | ||
| const ascReconnectAttemptedRef = useRef(false); |
There was a problem hiding this comment.
Reset recovery retry guards for each break
These counters live for the entire mounted hook and are never reset when enterBreak initializes a new lifecycle. If one ASC break consumes its reconnect attempt and later succeeds, a restoration failure on a subsequent break cannot retry after relogin; likewise, once the five legacy background attempts have been consumed, later breaks receive no background recovery. Reset both refs when a new break is accepted so the documented retry bounds apply per break rather than per widget mount.
Useful? React with 👍 / 👎.
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0cab52827a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| !this.store.isWellnessBreakEnabled || | ||
| !payload?.agentSessionId || | ||
| payload.agentId !== this.store.agentId || | ||
| !WELLNESS_NOTIFICATION_ACTIONS.has(payload.actionEvent) |
There was a problem hiding this comment.
Reject wellness events from other organizations
When the current profile has an organization ID, an SDK event with the same agentId but a different orgId passes this predicate and is exposed as an actionable offer. Accepting it then sends a response using the current tenant's local session, potentially acting on a foreign-organization notification; compare payload.orgId with this.store.agentProfile.orgId before storing the event.
Useful? React with 👍 / 👎.
| await api.respondToWellnessBreak({ | ||
| agentId: latest.agentId, | ||
| agentSessionId: latest.agentSessionId, | ||
| action: 'ACCEPTED', | ||
| }); |
There was a problem hiding this comment.
Abort acceptance when the active session rotates
If station relogin rotates agentSessionId while the preceding state-change request is pending, the session effect clears the old lifecycle, but this coroutine resumes with the latest snapshot captured before the await and sends ACCEPTED for the old session; it subsequently resurrects the old offer as waiting-for-safe-state in the new session. Capture an operation/session token before changing state and recheck it before responding, invoking callbacks, or advancing the phase.
Useful? React with 👍 / 👎.
| void groups.reduce( | ||
| (sequence, group) => | ||
| sequence.then(() => | ||
| store.cc |
There was a problem hiding this comment.
Continue restoring ASC groups after an unmount failure
When the widget unmounts during an ASC break with multiple restore groups, rejection of the first setAgentChannelState call rejects this .then chain, prevents every later group from running, and leaves the rejection unhandled. A transient failure restoring one channel can therefore leave all subsequent channels in WellbeingBreak; catch each group failure while preserving sequential execution so the remaining groups still receive a best-effort restore.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ece9358c66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| capturedRef.current = { | ||
| stateModel: marker.stateModel, | ||
| theme: input.theme, | ||
| channelTypes: marker.channelTypes || [], | ||
| preBreakChannelStates: marker.preBreakChannelStates || {}, |
There was a problem hiding this comment.
Preserve the legacy pre-break state across refresh recovery
When the page refreshes during a legacy break that began from an idle code such as Meeting, the recovery marker contains no preBreakLegacyState or preBreakLegacyAuxCodeId, and this reconstructed capture therefore leaves both undefined. performRestore() subsequently falls back to Available, making the agent routable instead of restoring the captured idle state. Persist and reconstruct the minimal legacy state fields alongside the ASC snapshot.
Useful? React with 👍 / 👎.
| if (input.rtdStatus.state === 'disconnected') { | ||
| clearOfferTimer(); | ||
| if (phaseRef.current === 'offer-pending' || phaseRef.current === 'request-pending') { | ||
| setRequestAvailable(false); | ||
| setPhase('idle'); |
There was a problem hiding this comment.
Deactivate the persisted offer on RTD disconnect
When RTD disconnects while an offer is pending, handleAIAssistantRtdStatus in storeEventsWrapper.ts has already changed the store phase to idle before this observer rerenders, so phaseRef.current is no longer offer-pending and this branch does not invalidate the offer history entry. The assistant then renders that persisted entry with enabled Take a break/Later actions, but both silently do nothing because the lifecycle is idle; resolve the active offer whenever the disconnect invalidates notification state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7d993a557
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Wellness state is changed by the AI Assistant lifecycle. Reflect its | ||
| // SDK event (and the following restore event) without echoing either | ||
| // transition back through setAgentState. | ||
| if (isCurrentStateExternallyManaged || externallyManagedStateRef.current) { |
There was a problem hiding this comment.
Send manual selections made from the wellness state
When the User State dropdown currently shows WellbeingBreak, externallyManagedStateRef.current remains true and the dropdown is still interactive. If the agent selects another state, isCurrentStateExternallyManaged becomes false but this condition still takes the early return, so updateAgentState is never called: the widget displays the selected state while the backend remains in WellbeingBreak. Distinguish user-originated selections from SDK restoration events, or disable the selector while the wellness state is externally owned.
Useful? React with 👍 / 👎.
| .wellness-break-overlay--assistant, | ||
| .wellness-break-overlay--custom { | ||
| padding: 0; | ||
| position: absolute; | ||
| } |
There was a problem hiding this comment.
Size assistant overlays independently of closed chrome
With wellnessBreakOverlayTarget="assistant", accepting from the toast while the assistant is closed or minimized leaves the overlay absolutely positioned against an .ai-assistant root whose height is only the launcher or minimized bar. The 5/60/5 break surface is therefore squeezed into that small area instead of a usable assistant-sized canvas. Give the root a stable active-overlay size or open/target the full panel before rendering this mode.
Useful? React with 👍 / 👎.
| fs.readdirSync(this.source) | ||
| .filter((filename) => filename.includes('lottie-web')) | ||
| .forEach((filename) => fs.copyFileSync(path.join(this.source, filename), path.join(this.destination, filename))); |
There was a problem hiding this comment.
Copy the renderer chunk in production builds
Under NODE_ENV=production, Webpack gives the unnamed import('lottie-web') chunk a deterministic ID-based filename rather than a name containing lottie-web. This filter consequently copies no renderer chunk from cc-components/dist, while the embedded component runtime still requests that file; published AI Assistant and aggregate builds then report MEDIA_UNAVAILABLE and never render the animation. Copy the referenced wellness chunks without relying on their generated names, or assign the import an explicit chunk name.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b39210972e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (phaseRef.current === 'request-pending') { | ||
| logWellnessMetric(WELLNESS_METRIC.PROVIDE_BREAK_EVENT_RECEIVED); | ||
| void enterBreak(event, false); |
There was a problem hiding this comment.
Elect one owner for singleton wellness events
When two AIAssistant widgets are mounted, both hook instances consume the same singleton-store sequence and can observe the same request-pending snapshot before either rerenders. A PROVIDE_WELLNESS_BREAK event therefore makes each instance call enterBreak, issuing duplicate setAgentState/setAgentChannelState requests and starting independent callbacks and timelines; direct offers similarly create one response timer per instance. Claim the event centrally or designate a single lifecycle owner before invoking enterBreak.
AGENTS.md reference: AGENTS.md:L87-L88
Useful? React with 👍 / 👎.
| </div> | ||
| ); | ||
|
|
||
| return overlayScope === 'custom' ? createPortal(overlay, resolvedTarget as HTMLElement) : overlay; |
There was a problem hiding this comment.
Portal viewport overlays outside the widget root
When the default viewport mode is used inside a transformed host container, returning the overlay inline makes its position: fixed box use that ancestor as its containing block and potentially inherit its clipping/stacking context. The purported viewport overlay can consequently cover only the embedded widget area rather than the browser viewport; portal viewport mode into the overlay's ownerDocument.body, while retaining the selected element for custom mode.
Useful? React with 👍 / 👎.
COMPLETES WXCC-12423
This pull request addresses
Adds Agent Wellness Break support to the React Contact Center widgets, aligned with the existing WXCC Desktop flows and presentation.
VIDCAST: https://app.vidcast.io/share/2d163aaa-6444-49a5-82fd-861d360b59c3
by making the following changes
The local SDK link and sample-development webpack/package changes used during development are intentionally excluded. Runtime integration depends on the matching @webex/contact-center Wellness Break API/event surface.
Change Type
The following scenarios were tested
The repository-wide style check completed with zero errors and six existing sample-app warnings.
The GAI Coding Policy And Copyright Annotation Best Practices
Checklist before merging
Make sure to have followed the contributing guidelines before submitting.