diff --git a/ai-docs/CONTRACTS.md b/ai-docs/CONTRACTS.md index d160f4f90..6a76ae085 100644 --- a/ai-docs/CONTRACTS.md +++ b/ai-docs/CONTRACTS.md @@ -26,6 +26,7 @@ The React entry of `@webex/cc-widgets` re-exports the widgets, including `AIAssi | ai-assistant.wellness-types | `@webex/cc-ai-assistant` | `WellnessBreakPhase`, `WellnessBreakErrorCode`, `WellnessBreakError`, `WellnessBreakOverlayTarget` | Exported TypeScript lifecycle, error, and layout contracts | additive minor surface; literals are stable | `packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md` | `packages/contact-center/ai-assistant/src/ai-assistant.types.ts` | | cc-components.WellnessBreak | `@webex/cc-components` | `WellnessBreakRequestCard`, `WellnessBreakOfferCard`, `WellnessBreakModal`, `WellnessBreakError` | Props-only Agent Wellness Break presentation; modal accepts an optional animation loader supplied by AI Assistant and owns no wellness media | monorepo component surface; no SDK/store or Lottie dependency | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | `packages/contact-center/cc-components/src/components/AIAssistant/WellnessBreak/` | | cc-widgets.store | `@webex/cc-store` | `store` | MobX singleton (`Store.getInstance()`); `init(options: InitParams, setupEventListeners): Promise`; sole SDK access point via `store.cc.*` | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/index.ts:1,4` | +| store.ai-summary | `@webex/cc-store` | `cc.aiSummaries`, summary request/receiver-feedback/send methods, `AISummaryEntry`, `AISummaryRole`, `AISummaryResponseSubmissionResult` | Entries keyed by interaction and role retain the unchanged SDK `AISummary` in `content` and `AISummaryResponse` in `response`. Requests return `AISummary` or `undefined`; senders return `AISummaryResponseSubmissionResult`. Widget helpers own local editing, copy tracking and deferred feedback through response fields. `setReceiverSummaryFeedback(feedback, expectedSummary, task)` uses the displayed SDK payload to guard its asynchronous acknowledgement. Consumers read enablement from `task.aiSummaryCapabilities`; `task:featureEnablement` refreshes the task view. Wrap-up sends `sendPostCallSummaryResponse(wrapUpCode, originalTask)` before `task.wrapup()`. | additive; uses published `@webex/contact-center` `3.12.0-next.148` | `packages/contact-center/store/ai-docs/store-spec.md` (AI summaries) | `packages/contact-center/store/src/store.types.ts`, `packages/contact-center/store/src/storeEventsWrapper.ts` | | store.host-init.enableWxBetterTogether | `@webex/cc-store` | `webexConfig.cc.enableWxBetterTogether` → `store.enableWxBetterTogether` | Host init boolean (default `false`), set **before** `store.init()`: read from `webexConfig.cc` on `{ webexConfig, access_token }` init or `webex.config.cc` on `{ webex }` init; read-only observable after init (Phase 1: re-init to change). Gates wxApp Mute/Keypad visibility and wxApp mute seed; SDK owns mute/DTMF API routing. | stable semver; renaming or removing the init key or store getter is breaking | `packages/contact-center/store/ai-docs/store-spec.md` (`STORE-R-030`); `packages/contact-center/task/ai-docs/task-spec.md` (WXCC-6026) | `packages/contact-center/store/src/store.ts`, `packages/contact-center/store/src/store.types.ts` (`WebexCcInitConfig`) | | store.types | `@webex/cc-store` | Existing queue/entry-point request, response, and entity type re-exports plus `TaskUIControls` | TypeScript exports describing the SDK-backed domain surface; established entity rows and Task destination controls pass through without a widget destination abstraction | stable semver; SDK-shaped types track SDK | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) (SDK source); `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts` | | cc-components.consult-transfer-lists | `@webex/cc-components` | `CallControl` consult/transfer fetch props plus `action`, `availableDestinations`, and optional list-item `presence` | `FetchPaginatedList` / `FetchPaginatedList` and the SDK-ordered destination array from `TaskUIControls`; UI preserves list/category order, passes buddy availability to Momentum Avatar as semantic presence, shows `AddressBookEntry.number` and `EntryPointRecord.number` subtitles, and may only apply host hide overrides | stable semver; entity and control types track store/SDK contracts; optional presentation prop is additive | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md`; `ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md` | `packages/contact-center/cc-components/src/components/task/task.types.ts` | diff --git a/packages/contact-center/store/ai-docs/store-spec.md b/packages/contact-center/store/ai-docs/store-spec.md index e0754dfd8..62958d102 100644 --- a/packages/contact-center/store/ai-docs/store-spec.md +++ b/packages/contact-center/store/ai-docs/store-spec.md @@ -48,7 +48,7 @@ Owns Contact Center client-side state and the SDK boundary: initialize/register ## Stack -TypeScript 5.6.3, MobX 6.13.5 (`makeAutoObservable`, `observable.ref`, `runInAction`). Consumed in React 18 via `mobx-react-lite` `observer()` in downstream packages (not a dependency of this package itself). SDK dependency `@webex/contact-center` 3.12.0-next.131. Tests: Jest 29 + ts compile (`tsc --project tsconfig.test.json && jest --coverage`). Build target: `dist/index.js` (Webpack). Evidence: `packages/contact-center/store/package.json`. +TypeScript 5.6.3, MobX 6.13.5 (`makeAutoObservable`, `observable.ref`, `runInAction`). Consumed in React 18 via `mobx-react-lite` `observer()` in downstream packages (not a dependency of this package itself). SDK dependency `@webex/contact-center` 3.12.0-next.148. Tests: Jest 29 + ts compile (`tsc --project tsconfig.test.json && jest --coverage`). Build target: `dist/index.js` (Webpack). Evidence: `packages/contact-center/store/package.json`. ## Folder / Package Structure @@ -94,7 +94,7 @@ Compatibility notes: ## Requires (dependencies) -- `@webex/contact-center` SDK (pinned in `package.json` at `3.12.0-next.131`) — the entire CC runtime: `Webex.init()`, `webex.cc.*` methods, the CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()`. Consumed ONLY through the store. Fallback on unavailability: `Store.init()` rejects after a 6000ms timeout (`src/store.ts:140-142`); the wrapper wraps the rejection and invokes `onErrorCallback('Store', err)` (`src/storeEventsWrapper.ts:442-452`). +- `@webex/contact-center` SDK (pinned in `package.json` at `3.12.0-next.148`) — the entire CC runtime: `Webex.init()`, `webex.cc.*` methods, the CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()`. Consumed ONLY through the store. Fallback on unavailability: `Store.init()` rejects after a 6000ms timeout (`src/store.ts:140-142`); the wrapper wraps the rejection and invokes `onErrorCallback('Store', err)` (`src/storeEventsWrapper.ts:442-452`). - `mobx` ^6.13.5 — observable state and `runInAction` for all mutations. - Internal: none upstream. The store is the lowest widget-layer dependency (`cc-components → widget packages → store → SDK`); it imports no widget package. @@ -397,6 +397,95 @@ Unit tests are split by source file. `tests/store.ts` covers the singleton defau | `STORE-R-030` | `tests/store.ts` (`enableWxBetterTogether` init paths and default) | none | | `STORE-R-031` | `tests/storeEventsWrapper.ts` ("should remove task callback even when task is absent from store.taskList", legacy string-id fallback cases) | none | +## AI summaries + +`StoreWrapper` is the SDK boundary for summaries. `cc.aiSummaries[interactionId][role]` +keeps the SDK summary, SDK response record and request state for `initiator`, `receiver`, +and `post-call`. Store requests and receiver events update these records inside +`runInAction`. Widget helpers own editing, copy tracking and deferred initiator/post-call +feedback through the SDK response fields; the store exposes no local UI action helpers. +A new SDK result replaces the unchanged `content` and initializes a fresh response. +Refreshing or failing a request retains +content already displayed and its response record; task removal and logout clear them. + +The store observes `cc` by reference. `registerCC` initializes the widget-owned +`cc.aiSummaries` map when the SDK does not provide one; it does not make SDK fields +observable. SDK events and request results update the map, while current consumers +read it through the summary APIs. A component that directly observes this map in the +future will need an observable state path. + +An agent can receive a transferred call and then consult or transfer it again. The +received card remains in AI Assistant while the initiator summary is used in the +consult/transfer modal, so their content, copies and feedback stay independent. +The post-call entry holds the wrap-up summary. Grouping these entries under the task's +interaction ID lets task cleanup remove them together; entries are created as needed. + +- `requestMidCallSummary(action, task)` and `requestPostCallSummary(task)` each call their + corresponding Task SDK method directly when `task.aiSummaryCapabilities` enables that + summary kind, and return the SDK `AISummary` or `undefined`. Like suggested responses, + summary requests and responses check the SDK flags directly, with no additional media-type + restriction. Disabled requests are quiet UI no-ops rather than SDK validation errors. + The SDK owns validation, transport correlation and timeouts. The store keeps a request + generation only to stop an older local Promise settlement, including a delayed SDK + publish failure, from overwriting a newer UI request. It does not correlate backend replies. +- `TASK_FEATURE_ENABLEMENT` (`task:featureEnablement`) refreshes the task view after the + SDK updates `task.aiSummaryCapabilities`. The current task is refreshed so observers + read the latest flags, including enablement arriving after the task and later disabling + the feature. The SDK owns the flags; the store adds no capability map or enablement helper. + `TASK_MID_CALL_SUMMARY_RECEIVED` supplies the receiver summary. Listeners are rebound + together when a task object is replaced, registered once per task object, and detached + on task removal and logout. +- Every role stores the complete SDK `AISummary` in `content`, including sections, + summary text, cards and SDK metadata. The store does not filter sections, trim text, + choose a display format or reject a received payload because of its format. Components + choose what they display from the SDK fields. Widget helpers keep edits in + `response.summary` and retain the original SDK content as their comparison baseline. + Consumers use the request status and SDK `areTranscriptsAvailable` flag to select a + display state. A failed refresh keeps previously received content visible. Invalid + transport payloads belong to the SDK boundary. +- Each entry stores an SDK `AISummaryResponse` in `response`. Widget helpers update its + copy, edit and deferred-feedback fields; senders forward that record to the SDK. + The SDK sends these fields verbatim and supplies no runtime defaults. Received + summaries start with `DEFAULT` and one view. Requests without content start with + `IGNORED`, including retries; failure without content changes the state to `NOT_RECEIVED`. + Structured responses start with `{}` and plain-text responses start with SDK summary text. +- `setReceiverSummaryFeedback(feedback, expectedSummary, task)` submits receiver feedback + immediately. It accepts the displayed SDK payload itself, with no revision counter. + The store blocks overlapping sends and applies the acknowledgement only while that + same SDK payload remains current. Replacement events and task removal invalidate it. + A successful send selects the feedback; a failed send preserves the previous selection. + Receiver responses use SDK `summaryText`, including when the payload also has sections + or an adaptive card; absent text is sent as `''`. +- `sendMidCallSummaryResponse(action, task)` and `sendPostCallSummaryResponse(wrapUpCode, task)` + return `AISummaryResponseSubmissionResult`: `submitted` after SDK acknowledgement, + `response-failed` after a failed send, or `not-applicable` when the required task or + summary capability is absent. Failed sends are logged and resolve with an outcome so + the initiating consult, transfer or wrap-up action can continue. +- `sendPostCallSummaryResponse(wrapUpCode, task)` adds the wrap-up code to the SDK response + record and sends it on that task before `task.wrapup()` runs. This keeps the exact task + context available and removes the need for callers to capture and later submit a response. + If no summary entry exists, it sends an ignored response without creating an entry. It + resolves to `submitted` or `response-failed`; disabled post-call summaries return + `not-applicable`. + +The integration consumes `AISummary`, `AISummarySections`, `AISummaryResponse`, +`AISummaryAction`, and `AISummaryFeedback` from `@webex/contact-center`. +These APIs come from published SDK `3.12.0-next.148`. The store and React sample pin +that version; the root resolution also selects it for the sample's transitive Webex +SDK dependency. Three scoped resolutions replace unavailable transitive versions: +Device `3.12.0-next.55` uses published `next.54`, and Metrics `3.12.0-next.56` uses +published `next.55`; Mercury `3.12.0-next.62` uses published `next.61`. Remove these +resolutions once the referenced versions are published. No local SDK archive is used. +The React sample uses the SDK-supported `User requested logout` reason for page-unload logout. +`AISummary` is the common incoming payload type; `AISummaryResponse` is the distinct +feedback payload sent back to the SDK. The store retains both types directly. The UI +entry adds request lifecycle, local request generation, the pending receiver-feedback +flag and the consult/transfer action. Copy counts, edits and feedback +use SDK response fields rather than duplicate widget fields. + +Evidence: `src/storeEventsWrapper.ts`, `src/store.types.ts`, `src/task-utils.ts`; +tests: the AI summary cases in `tests/storeEventsWrapper.ts` and `tests/task-utils.ts`. + ## Traceability - Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index e4ab7cc87..c3b068eea 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -23,7 +23,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/contact-center": "3.12.0-next.133", + "@webex/contact-center": "3.12.0-next.148", "mobx": "6.13.5", "typescript": "5.6.3" }, diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 4b130ef5a..4537ca00a 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -97,6 +97,7 @@ class Store implements IStore { registerCC(webex?: WithWebex['webex']): Promise { if (webex) { this.cc = webex.cc; + this.cc.aiSummaries ??= {}; } if (typeof webex === 'undefined' && typeof this.cc === 'undefined') { diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 0e55e6cea..add56eb5d 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -31,6 +31,13 @@ import { WellnessBreakEvent, RespondToWellnessBreakParams, } from '@webex/contact-center'; +import type { + AISummary, + AISummaryAction, + AISummaryFeedback, + AISummaryResponse, + AISummarySections, +} from '@webex/contact-center'; import type {RealTimeAssistanceParams} from 'node_modules/@webex/contact-center/dist/types/types'; import { OutdialAniEntriesResponse, @@ -52,6 +59,8 @@ import { // To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 interface IContactCenter { + /** Widget-owned AI summary state associated with this SDK contact-center instance. */ + aiSummaries: Record>>; // eslint-disable-next-line @typescript-eslint/no-explicit-any on: (event: string, callback: (data: any) => void) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -319,6 +328,7 @@ interface IStore { wellnessEventSequence: number; legacyAgentState: string; legacyAuxCodeId: string; + init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise; registerCC(webex?: WithWebex['webex']): Promise; loadWellbeingBreakIdleCode(): Promise; @@ -371,6 +381,16 @@ interface IStoreWrapper extends IStore { setWellnessBreakState(state: WellnessBreakState): void; submitBehavioralMetric(metric: WidgetsBehavioralMetric): void; resetWellnessSession(): void; + + requestMidCallSummary(action: AISummaryAction, task?: ITask): Promise; + requestPostCallSummary(task?: ITask): Promise; + setReceiverSummaryFeedback( + feedback: Exclude, + expectedSummary: AISummary, + task?: ITask + ): Promise; + sendMidCallSummaryResponse(action: AISummaryAction, task?: ITask): Promise; + sendPostCallSummaryResponse(wrapUpCode: string, task: ITask): Promise; } interface IWrapupCode { @@ -524,6 +544,10 @@ export type { WidgetsBehavioralMetric, WidgetsBehavioralMetricAgent, WidgetsBehavioralMetricVerb, + AISummaryAction, + AISummaryFeedback, + AISummaryResponse, + AISummarySections, }; export { @@ -649,3 +673,27 @@ export const CAMPAIGN_PREVIEW_OUTBOUND_TYPES = ['STANDARD_PREVIEW_CAMPAIGN', 'DI /** Campaign type values (from callProcessingDetails) that identify a campaign preview task. */ export const CAMPAIGN_PREVIEW_CAMPAIGN_TYPES = ['preview_standard', 'preview_direct']; + +/** The consult/transfer initiator's summary, the receiving agent's summary, or the wrap-up summary. */ +export type AISummaryRole = 'initiator' | 'receiver' | 'post-call'; + +/** One summary of an interaction, stored at `cc.aiSummaries[interactionId][role]`. */ +export type AISummaryEntry = { + /** Lifecycle of the latest request, or of the pushed receiver summary. */ + status: 'loading' | 'ready' | 'error'; + /** + * The unchanged SDK summary, retained while a newer request loads or after it fails. + */ + content?: AISummary; + /** SDK submission record; widget helpers own local edits, copy counts and deferred feedback. */ + response: AISummaryResponse; + /** Orders local request promise settlements; SDK transport correlation remains SDK-owned. */ + requestGeneration?: number; + /** The receiving agent's feedback response is being sent. */ + feedbackPending?: boolean; + /** The consult or transfer a mid-call summary was generated for. */ + action?: AISummaryAction; +}; + +/** Outcome of sending a mid-call or post-call summary response through the SDK. */ +export type AISummaryResponseSubmissionResult = 'submitted' | 'response-failed' | 'not-applicable'; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 071070f7e..69caabc0b 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -33,7 +33,9 @@ import { WidgetsBehavioralMetric, WELLNESS_BREAK_NOTIFICATION_ACTIONS, } from './store.types'; +import type {AISummaryEntry, AISummaryResponseSubmissionResult} from './store.types'; import Store from './store'; +import type {AISummary, AISummaryAction, AISummaryFeedback, AISummaryResponse} from '@webex/contact-center'; import { DEVICE_TYPE_BROWSER, MEDIA_TYPE_TELEPHONY_LOWER, @@ -42,9 +44,18 @@ import { CAMPAIGN_PREVIEW_CAMPAIGN_TYPES, } from './store.types'; import {runInAction} from 'mobx'; -import {isIncomingTask} from './task-utils'; +import {isIncomingTask, isSecondaryAgent} from './task-utils'; import {SUGGESTED_RESPONSE_EVENT, TASK_MULTI_LOGIN_HYDRATE} from './constants'; +const EMPTY_AI_SUMMARY_RESPONSE: AISummaryResponse = { + summary: '', + feedback: 'none', + state: 'IGNORED', + numberOfTimesViewed: 0, + numberOfTimesEdited: 0, + numberOfTimesCopied: 0, +}; + const CONSULT_TRANSFER_CHANNELS = { telephony: 'TELEPHONY', chat: 'CHAT', @@ -87,6 +98,9 @@ class StoreWrapper implements IStoreWrapper { private wxAppMuteStateListeners: Record void}> = {}; private taskEndListeners: Record void}> = {}; private muteStateByInteractionId: Record = {}; + // Keep the task and callback so replacement, removal, and logout can detach the exact listener. + private aiSummaryReceivedListeners: Record void}> = {}; + private aiSummaryRequestGeneration = 0; constructor() { this.store = Store.getInstance(); @@ -460,6 +474,245 @@ class StoreWrapper implements IStoreWrapper { }); }; + // The SDK owns validation, transport correlation and timeouts. This generation only + // prevents an older local promise settlement from overwriting the latest UI request. + + requestMidCallSummary = async ( + action: AISummaryAction, + task: ITask = this.currentTask + ): Promise => { + // Disabled summaries are a quiet UI no-op; the SDK validates enabled requests. + if (!task?.aiSummaryCapabilities?.midCallEnabled) { + return undefined; + } + const interactionId = task.data.interactionId; + const requestGeneration = ++this.aiSummaryRequestGeneration; + const entry = this.store.cc.aiSummaries[interactionId]?.initiator; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: { + ...this.store.cc.aiSummaries[interactionId], + initiator: { + ...entry, + response: entry?.content ? entry.response : {...EMPTY_AI_SUMMARY_RESPONSE}, + status: 'loading', + requestGeneration, + }, + }, + }; + }); + + let result: AISummary | undefined; + try { + result = await task.requestMidCallSummary(action); + } catch (error) { + this.store.logger?.error(`CC-Widgets: AI summary request failed: ${error}`, { + module: 'storeEventsWrapper.ts', + method: 'request', + }); + } + const current = this.store.cc.aiSummaries[interactionId]?.initiator; + if (current?.requestGeneration !== requestGeneration) { + return undefined; + } + + // A new result resets edits, copies and feedback; a failed refresh keeps the summary already shown. + const updated: AISummaryEntry = result + ? { + status: 'ready', + content: result, + response: { + ...EMPTY_AI_SUMMARY_RESPONSE, + summary: result.sections ? {} : (result.summaryText ?? ''), + state: 'DEFAULT', + numberOfTimesViewed: 1, + }, + action, + } + : { + ...current, + status: 'error', + response: current.content ? current.response : {...current.response, state: 'NOT_RECEIVED'}, + action: current.action ?? action, + }; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: {...this.store.cc.aiSummaries[interactionId], initiator: updated}, + }; + }); + return result; + }; + + requestPostCallSummary = async (task: ITask = this.currentTask): Promise => { + if (!task?.aiSummaryCapabilities?.postCallEnabled) { + return undefined; + } + const interactionId = task.data.interactionId; + const requestGeneration = ++this.aiSummaryRequestGeneration; + const entry = this.store.cc.aiSummaries[interactionId]?.['post-call']; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: { + ...this.store.cc.aiSummaries[interactionId], + 'post-call': { + ...entry, + response: entry?.content ? entry.response : {...EMPTY_AI_SUMMARY_RESPONSE}, + status: 'loading', + requestGeneration, + }, + }, + }; + }); + + let result: AISummary | undefined; + try { + result = await task.requestPostCallSummary(); + } catch (error) { + this.store.logger?.error(`CC-Widgets: AI summary request failed: ${error}`, { + module: 'storeEventsWrapper.ts', + method: 'request', + }); + } + const current = this.store.cc.aiSummaries[interactionId]?.['post-call']; + if (current?.requestGeneration !== requestGeneration) { + return undefined; + } + + // A new result resets edits, copies and feedback; a failed refresh keeps the summary already shown. + const updated: AISummaryEntry = result + ? { + status: 'ready', + content: result, + response: { + ...EMPTY_AI_SUMMARY_RESPONSE, + summary: result.sections ? {} : (result.summaryText ?? ''), + state: 'DEFAULT', + numberOfTimesViewed: 1, + }, + } + : { + ...current, + status: 'error', + response: current.content ? current.response : {...current.response, state: 'NOT_RECEIVED'}, + }; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: {...this.store.cc.aiSummaries[interactionId], 'post-call': updated}, + }; + }); + return result; + }; + + /** The receiving agent's feedback is sent right away; the selection is kept once the SDK confirms it. */ + setReceiverSummaryFeedback = async ( + feedback: Exclude, + expectedSummary: AISummary, + task: ITask = this.currentTask + ): Promise => { + const entry = task && this.store.cc.aiSummaries[task.data.interactionId]?.receiver; + if (!entry?.content || !entry.action || entry.content !== expectedSummary || entry.feedbackPending) { + return; + } + const interactionId = task.data.interactionId; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: { + ...this.store.cc.aiSummaries[interactionId], + receiver: {...entry, feedbackPending: true}, + }, + }; + }); + + let sent = true; + try { + await task.sendMidCallSummaryResponse({...entry.response, feedback}, entry.action); + } catch (error) { + sent = false; + this.store.logger?.error(`CC-Widgets: AI summary feedback failed: ${error}`, { + module: 'storeEventsWrapper.ts', + method: 'feedback', + }); + } + + const current = this.store.cc.aiSummaries[interactionId]?.receiver; + // A new summary replaced this one, or the task was removed, while the response was in flight. + if (current?.content === expectedSummary && current.feedbackPending) { + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: { + ...this.store.cc.aiSummaries[interactionId], + receiver: { + ...current, + response: sent ? {...current.response, feedback} : current.response, + feedbackPending: false, + }, + }, + }; + }); + } + }; + + /** + * Reports the initiator's summary for the consult or transfer that is starting and returns the submission outcome. + * A failed send is reported without rejecting, so the consult or transfer can continue. + */ + sendMidCallSummaryResponse = async ( + action: AISummaryAction, + task: ITask = this.currentTask + ): Promise => { + if (!task?.aiSummaryCapabilities?.midCallEnabled) { + return 'not-applicable'; + } + try { + await task.sendMidCallSummaryResponse( + this.store.cc.aiSummaries[task.data.interactionId]?.initiator?.response ?? EMPTY_AI_SUMMARY_RESPONSE, + action + ); + return 'submitted'; + } catch (error) { + this.store.logger?.error(`CC-Widgets: AI summary response failed: ${error}`, { + module: 'storeEventsWrapper.ts', + method: 'response', + }); + return 'response-failed'; + } + }; + + /** Adds the wrap-up code to the post-call response and sends it before the task is wrapped up. */ + sendPostCallSummaryResponse = async (wrapUpCode: string, task: ITask): Promise => { + if (!task.aiSummaryCapabilities?.postCallEnabled) { + return 'not-applicable'; + } + const interactionId = task.data.interactionId; + const entry = this.store.cc.aiSummaries[interactionId]?.['post-call']; + const response = {...(entry?.response ?? EMPTY_AI_SUMMARY_RESPONSE), wrapUpCode}; + if (entry) { + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: {...this.store.cc.aiSummaries[interactionId], 'post-call': {...entry, response}}, + }; + }); + } + let outcome: 'submitted' | 'response-failed' = 'submitted'; + try { + await task.sendPostCallSummaryResponse(response); + } catch (error) { + outcome = 'response-failed'; + this.store.logger?.error(`CC-Widgets: AI summary response failed: ${error}`, { + module: 'storeEventsWrapper.ts', + method: 'response', + }); + } + return outcome; + }; + setCurrentTheme = (theme: string): void => { this.store.currentTheme = theme; }; @@ -876,6 +1129,12 @@ class StoreWrapper implements IStoreWrapper { (listenerTask ?? taskToRemove).off(SUGGESTED_RESPONSE_EVENT, listener); delete this.realTimeAssistListeners[taskId]; } + if (taskId && this.aiSummaryReceivedListeners[taskId]) { + const {task: listenerTask, listener} = this.aiSummaryReceivedListeners[taskId]; + listenerTask.off(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, listener); + listenerTask.off(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, this.refreshTaskList); + delete this.aiSummaryReceivedListeners[taskId]; + } if (taskId && this.store.realTimeAssist && this.store.realTimeAssist[taskId]) { runInAction(() => { const next = {...this.store.realTimeAssist}; @@ -883,6 +1142,13 @@ class StoreWrapper implements IStoreWrapper { this.store.realTimeAssist = next; }); } + if (taskId && this.store.cc.aiSummaries[taskId]) { + runInAction(() => { + const next = {...this.store.cc.aiSummaries}; + delete next[taskId]; + this.store.cc.aiSummaries = next; + }); + } } runInAction(() => { @@ -1378,6 +1644,38 @@ class StoreWrapper implements IStoreWrapper { this.realTimeAssistListeners[taskId] = {task, listener}; task.on(SUGGESTED_RESPONSE_EVENT, listener); } + const existingSummary = this.aiSummaryReceivedListeners[taskId]; + if (existingSummary?.task !== task) { + existingSummary?.task.off(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, existingSummary.listener); + existingSummary?.task.off(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, this.refreshTaskList); + // Receiver summaries are pushed regardless of this agent's feature enablement. + const listener = (payload: AISummary) => { + const interactionId = task.data.interactionId; + runInAction(() => { + this.store.cc.aiSummaries = { + ...this.store.cc.aiSummaries, + [interactionId]: { + ...this.store.cc.aiSummaries[interactionId], + receiver: { + status: 'ready', + content: payload, + response: { + ...EMPTY_AI_SUMMARY_RESPONSE, + summary: payload.summaryText ?? '', + state: 'DEFAULT', + numberOfTimesViewed: 1, + }, + action: isSecondaryAgent(task) ? 'CONSULT' : 'TRANSFER', + }, + }, + }; + }); + }; + this.aiSummaryReceivedListeners[taskId] = {task, listener}; + task.on(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, listener); + // The SDK updates task.aiSummaryCapabilities before emitting; refresh so widgets read the new flags. + task.on(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, this.refreshTaskList); + } } }; @@ -1750,6 +2048,13 @@ class StoreWrapper implements IStoreWrapper { this.store.realTimeAssist = {}; this.realTimeAssistListeners = {}; this.resetWellnessSession(); + + Object.values(this.aiSummaryReceivedListeners).forEach(({task, listener}) => { + task.off(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, listener); + task.off(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, this.refreshTaskList); + }); + this.aiSummaryReceivedListeners = {}; + this.store.cc.aiSummaries = {}; }); }; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 6a463e5b8..bc3c18929 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -23,7 +23,9 @@ console.error = jest.fn(); console.log = jest.fn(); import {CC_EVENTS, TASK_EVENTS} from '../src/store.types'; -import type {RealTimeAssistPayload} from '../src/store.types'; +import type {AISummaryAction, AISummaryRole, RealTimeAssistPayload} from '../src/store.types'; +import type {AISummary, AISummaryResponse} from '@webex/contact-center'; +import {aiSummaryFixtures} from '../../test-fixtures/src/aiSummaryFixtures'; import storeWrapper from '../src/storeEventsWrapper'; import {ITask} from '../src/store.types'; import {getConferenceParticipantDropRoster} from '../src/task-utils'; @@ -62,6 +64,7 @@ jest.mock('../src/store', () => ({ teams: 'mockTeams', loginOptions: 'mockLoginOptions', cc: { + aiSummaries: {}, on: jest.fn(), off: jest.fn(), addressBook: { @@ -3876,6 +3879,717 @@ describe('storeEventsWrapper', () => { }); }); + describe('AI summaries', () => { + const INTERACTION_ID = 'interaction-main-1'; + + type AISummaryTestTask = jest.Mocked; + + const makeAISummaryTask = ( + interaction: Record = {}, + aiSummaryCapabilities = {midCallEnabled: true, postCallEnabled: true} + ): AISummaryTestTask => + ({ + data: { + interactionId: INTERACTION_ID, + interaction: {interactionId: INTERACTION_ID, mediaType: 'telephony', ...interaction}, + }, + aiSummaryCapabilities, + on: jest.fn(), + off: jest.fn(), + requestMidCallSummary: jest.fn().mockResolvedValue(aiSummaryFixtures.initiatingMidCall.typedSections), + requestPostCallSummary: jest.fn().mockResolvedValue(aiSummaryFixtures.postCall.plainText), + sendMidCallSummaryResponse: jest.fn().mockResolvedValue(undefined), + sendPostCallSummaryResponse: jest.fn().mockResolvedValue(undefined), + }) as unknown as AISummaryTestTask; + + const deferred = () => { + let resolve: (value: T) => void = () => undefined; + let reject: (reason?: unknown) => void = () => undefined; + const promise = new Promise((promiseResolve, promiseReject) => { + resolve = promiseResolve; + reject = promiseReject; + }); + return {promise, resolve, reject}; + }; + + const summaryError = (errorCode: string) => Object.assign(new Error(errorCode), {data: {errorCode}}); + + const entryFor = (role: AISummaryRole) => storeWrapper.cc.aiSummaries[INTERACTION_ID]?.[role]; + + const registerTaskEventListeners = (task: ITask) => + (storeWrapper as unknown as {registerTaskEventListeners: (t: ITask) => void}).registerTaskEventListeners(task); + + const receivedListenerOf = (task: AISummaryTestTask) => + task.on.mock.calls.find(([event]) => event === TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED)?.[1]; + + beforeEach(() => { + storeWrapper['store'].cc.aiSummaries = {}; + storeWrapper['aiSummaryReceivedListeners'] = {}; + }); + + afterEach(() => { + storeWrapper['store'].cc.aiSummaries = {}; + }); + + describe('requests', () => { + it('should store the initiator summary and its action once the request settles', async () => { + const task = makeAISummaryTask(); + const reply = deferred(); + task.requestMidCallSummary.mockReturnValue(reply.promise); + + const request = storeWrapper.requestMidCallSummary('CONSULT', task); + + expect(task.requestMidCallSummary).toHaveBeenCalledWith('CONSULT'); + expect(entryFor('initiator')).toMatchObject({status: 'loading'}); + + reply.resolve(aiSummaryFixtures.initiatingMidCall.typedSections); + await request; + + expect(entryFor('initiator')).toMatchObject({ + status: 'ready', + action: 'CONSULT', + content: {sections: expect.any(Object)}, + }); + expect(entryFor('initiator').content).toEqual(aiSummaryFixtures.initiatingMidCall.typedSections); + }); + + it('should preserve SDK fields without filtering sections or choosing a display format', async () => { + const task = makeAISummaryTask(); + const payload: AISummary = { + ...aiSummaryFixtures.initiatingMidCall.typedSections, + sections: {reasonForTransferOrConsult: '', nextSteps: 'Follow up tomorrow.'}, + editAdaptiveCard: {type: 'AdaptiveCard'}, + suggestedWrapUpCodes: [{name: 'Billing'}], + }; + task.requestMidCallSummary.mockResolvedValueOnce(payload); + + await storeWrapper.requestMidCallSummary('CONSULT', task); + + expect(entryFor('initiator').content).toBe(payload); + }); + + it('should not request a summary kind the SDK reports as disabled for the task', async () => { + const postCallOnly = makeAISummaryTask({}, {midCallEnabled: false, postCallEnabled: true}); + const midCallOnly = makeAISummaryTask({}, {midCallEnabled: true, postCallEnabled: false}); + + await storeWrapper.requestMidCallSummary('TRANSFER', postCallOnly); + await storeWrapper.requestPostCallSummary(midCallOnly); + + expect(postCallOnly.requestMidCallSummary).not.toHaveBeenCalled(); + expect(midCallOnly.requestPostCallSummary).not.toHaveBeenCalled(); + expect(storeWrapper.cc.aiSummaries).toEqual({}); + }); + + it('should use SDK summary enablement without filtering the task media type', async () => { + const task = makeAISummaryTask({mediaType: 'chat'}); + + await storeWrapper.requestMidCallSummary('CONSULT', task); + await storeWrapper.requestPostCallSummary(task); + await storeWrapper.sendMidCallSummaryResponse('CONSULT', task); + + expect(task.requestMidCallSummary).toHaveBeenCalledWith('CONSULT'); + expect(task.requestPostCallSummary).toHaveBeenCalledTimes(1); + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith(entryFor('initiator').response, 'CONSULT'); + expect(entryFor('post-call').response.wrapUpCode).toBeUndefined(); + }); + + it('should retain the newest summary when an older request fails to publish', async () => { + const task = makeAISummaryTask(); + const replaced = deferred(); + task.requestMidCallSummary + .mockReturnValueOnce(replaced.promise) + .mockResolvedValueOnce(aiSummaryFixtures.initiatingMidCall.plainText); + + const first = storeWrapper.requestMidCallSummary('CONSULT', task); + await storeWrapper.requestMidCallSummary('TRANSFER', task); + + expect(entryFor('initiator')).toMatchObject({ + status: 'ready', + action: 'TRANSFER', + content: {summaryText: expect.any(String)}, + }); + + replaced.reject(new Error('Publishing the replaced SDK request failed')); + await first; + + expect(entryFor('initiator')).toMatchObject({status: 'ready', action: 'TRANSFER'}); + }); + + it('should keep the summary already shown when a refresh fails', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestMidCallSummary('CONSULT', task); + const shown = entryFor('initiator'); + task.requestMidCallSummary.mockRejectedValueOnce(summaryError('MID_CALL_SUMMARY_TIMEOUT')); + + await storeWrapper.requestMidCallSummary('TRANSFER', task); + + expect(entryFor('initiator')).toMatchObject({ + status: 'error', + content: shown.content, + response: shown.response, + action: 'CONSULT', + }); + }); + + it.each(['initiator', 'post-call'] as const)( + 'should ignore an older %s publishing failure while the newest request is still loading', + async (role) => { + const task = makeAISummaryTask(); + const older = deferred(); + const newer = deferred(); + const sdkRequest = role === 'initiator' ? task.requestMidCallSummary : task.requestPostCallSummary; + sdkRequest.mockReturnValueOnce(older.promise).mockReturnValueOnce(newer.promise); + const request = (action: AISummaryAction) => + role === 'initiator' + ? storeWrapper.requestMidCallSummary(action, task) + : storeWrapper.requestPostCallSummary(task); + + const first = request('CONSULT'); + const latest = request('TRANSFER'); + older.reject(new Error('Publishing the replaced SDK request failed')); + await first; + + expect(entryFor(role)).toMatchObject({status: 'loading'}); + + newer.resolve(aiSummaryFixtures.initiatingMidCall.plainText); + await latest; + + expect(entryFor(role)).toMatchObject({ + status: 'ready', + content: {summaryText: aiSummaryFixtures.initiatingMidCall.plainText.summaryText}, + }); + } + ); + + it('should record a failed first request', async () => { + const task = makeAISummaryTask(); + task.requestPostCallSummary.mockRejectedValueOnce(summaryError('POST_CALL_SUMMARY_TIMEOUT')); + + await storeWrapper.requestPostCallSummary(task); + + expect(entryFor('post-call')).toMatchObject({status: 'error', response: {state: 'NOT_RECEIVED'}}); + expect(storeWrapper['store'].logger.error).toHaveBeenCalledWith( + 'CC-Widgets: AI summary request failed: Error: POST_CALL_SUMMARY_TIMEOUT', + {module: 'storeEventsWrapper.ts', method: 'request'} + ); + }); + + it('should retain a card-only SDK reply for the UI to render', async () => { + const task = makeAISummaryTask(); + task.requestPostCallSummary.mockResolvedValueOnce(aiSummaryFixtures.postCall.cardOnlyUnsupported); + + await storeWrapper.requestPostCallSummary(task); + + expect(entryFor('post-call')).toMatchObject({ + status: 'ready', + content: aiSummaryFixtures.postCall.cardOnlyUnsupported, + }); + }); + + it('should retain the SDK reply when transcripts are unavailable', async () => { + const task = makeAISummaryTask(); + const payload: AISummary = {conversationId: INTERACTION_ID, areTranscriptsAvailable: false}; + task.requestPostCallSummary.mockResolvedValueOnce(payload); + + await storeWrapper.requestPostCallSummary(task); + + expect(entryFor('post-call')).toMatchObject({status: 'ready', content: payload}); + }); + + it('should drop a reply that arrives after the task was removed', async () => { + const task = makeAISummaryTask(); + const reply = deferred(); + task.requestMidCallSummary.mockReturnValue(reply.promise); + + const request = storeWrapper.requestMidCallSummary('CONSULT', task); + storeWrapper.handleTaskRemove(task); + reply.resolve(aiSummaryFixtures.initiatingMidCall.typedSections); + await request; + + expect(storeWrapper.cc.aiSummaries[INTERACTION_ID]).toBeUndefined(); + }); + + it('should do nothing without a task', async () => { + await storeWrapper.requestPostCallSummary(undefined); + + expect(storeWrapper.cc.aiSummaries).toEqual({}); + }); + }); + + describe('receiving agent summary', () => { + it('should store the adaptive card pushed for the task with the consult or transfer it belongs to', () => { + const transferTask = makeAISummaryTask(); + registerTaskEventListeners(transferTask); + + receivedListenerOf(transferTask)(aiSummaryFixtures.receivingMidCall.adaptiveCard); + + expect(entryFor('receiver')).toMatchObject({ + status: 'ready', + action: 'TRANSFER', + content: {adaptiveCard: expect.any(Object)}, + }); + expect(entryFor('receiver').content).toBe(aiSummaryFixtures.receivingMidCall.adaptiveCard); + + const consultTask = makeAISummaryTask({ + callProcessingDetails: {relationshipType: 'consult', parentInteractionId: 'parent-interaction'}, + }); + registerTaskEventListeners(consultTask); + receivedListenerOf(consultTask)(aiSummaryFixtures.receivingMidCall.adaptiveCard); + + expect(entryFor('receiver')).toMatchObject({action: 'CONSULT'}); + }); + + it('should retain a pushed SDK summary without requiring an adaptive card', () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + + receivedListenerOf(task)(aiSummaryFixtures.receivingMidCall.typedOnlyUnsupported); + + expect(entryFor('receiver')).toMatchObject({ + status: 'ready', + content: aiSummaryFixtures.receivingMidCall.typedOnlyUnsupported, + }); + }); + + it('should rebind the listener for a replacement task object and remove it with the task', () => { + const task = makeAISummaryTask(); + const replacement = makeAISummaryTask(); + registerTaskEventListeners(task); + registerTaskEventListeners(task); + const listener = receivedListenerOf(task); + + registerTaskEventListeners(replacement); + + expect( + task.on.mock.calls.filter(([event]) => event === TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED) + ).toHaveLength(1); + expect(task.on.mock.calls.filter(([event]) => event === TASK_EVENTS.TASK_FEATURE_ENABLEMENT)).toHaveLength(1); + expect(task.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, listener); + expect(task.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, storeWrapper.refreshTaskList); + + storeWrapper.handleTaskRemove(replacement); + + expect(replacement.off).toHaveBeenCalledWith( + TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, + receivedListenerOf(replacement) + ); + expect(storeWrapper['aiSummaryReceivedListeners'][INTERACTION_ID]).toBeUndefined(); + }); + + it('should detach both summary listeners on logout', () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + const listener = receivedListenerOf(task); + + storeWrapper.cleanUpStore(); + + expect(task.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, listener); + expect(task.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, storeWrapper.refreshTaskList); + expect(storeWrapper['aiSummaryReceivedListeners']).toEqual({}); + }); + }); + + it('should use the latest SDK flags after task:featureEnablement refreshes the task', async () => { + const disabled = {midCallEnabled: false, postCallEnabled: false}; + const enabled = {midCallEnabled: true, postCallEnabled: true}; + const task = makeAISummaryTask({}, disabled); + const previousTask = storeWrapper.currentTask; + const previousTaskList = storeWrapper.taskList; + const taskManager = storeWrapper['store'].cc.taskManager; + const previousGetAllTasks = taskManager.getAllTasks; + const getAllTasks = jest.fn().mockReturnValue({[INTERACTION_ID]: task}); + taskManager.getAllTasks = getAllTasks; + + try { + registerTaskEventListeners(task); + storeWrapper.setCurrentTask(task); + const shownTask = storeWrapper.currentTask; + const featureListener = task.on.mock.calls.find(([event]) => event === TASK_EVENTS.TASK_FEATURE_ENABLEMENT)[1]; + + await storeWrapper.requestMidCallSummary('CONSULT'); + await storeWrapper.requestPostCallSummary(); + expect(task.requestMidCallSummary).not.toHaveBeenCalled(); + expect(task.requestPostCallSummary).not.toHaveBeenCalled(); + + // SDK updates its flags before emitting the event; the store reads those flags after refresh. + Object.assign(task, {aiSummaryCapabilities: enabled}); + featureListener({interactionId: INTERACTION_ID, ...enabled}); + expect(storeWrapper.currentTask).not.toBe(shownTask); + expect(storeWrapper.currentTask.aiSummaryCapabilities).toEqual(enabled); + + await storeWrapper.requestMidCallSummary('CONSULT'); + await storeWrapper.requestPostCallSummary(); + await storeWrapper.sendMidCallSummaryResponse('CONSULT'); + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', task)).resolves.toBe('submitted'); + + Object.assign(task, {aiSummaryCapabilities: disabled}); + featureListener({interactionId: INTERACTION_ID, ...disabled}); + expect(storeWrapper.currentTask.aiSummaryCapabilities).toEqual(disabled); + await storeWrapper.requestMidCallSummary('CONSULT'); + await storeWrapper.requestPostCallSummary(); + await storeWrapper.sendMidCallSummaryResponse('CONSULT'); + + expect(task.requestMidCallSummary).toHaveBeenCalledTimes(1); + expect(task.requestPostCallSummary).toHaveBeenCalledTimes(1); + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledTimes(1); + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', task)).resolves.toBe('not-applicable'); + + getAllTasks.mockReturnValue({}); + storeWrapper.handleTaskRemove(task); + expect(task.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, storeWrapper.refreshTaskList); + } finally { + taskManager.getAllTasks = previousGetAllTasks; + storeWrapper['store'].currentTask = previousTask; + storeWrapper['store'].taskList = previousTaskList; + } + }); + + describe('caller-owned response records', () => { + it('should forward the initiator response without changing caller-owned edits, counts or feedback', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestMidCallSummary('CONSULT', task); + const response: AISummaryResponse = { + summary: {additionalContext: '', keyActionsTaken: 'Edited actions'}, + feedback: 'thumbs_up', + state: 'DEFAULT', + numberOfTimesViewed: 2, + numberOfTimesEdited: 1, + numberOfTimesCopied: 3, + }; + entryFor('initiator').response = response; + + await storeWrapper.sendMidCallSummaryResponse('TRANSFER', task); + + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith(response, 'TRANSFER'); + expect(task.sendMidCallSummaryResponse.mock.calls[0][0]).toBe(response); + expect(entryFor('initiator').content).toBe(aiSummaryFixtures.initiatingMidCall.typedSections); + }); + + it('should start a refreshed summary with a new response while retaining the raw SDK payload', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestPostCallSummary(task); + entryFor('post-call').response = { + ...entryFor('post-call').response, + summary: 'Caller-edited text', + numberOfTimesCopied: 2, + numberOfTimesEdited: 1, + feedback: 'thumbs_up', + }; + const refreshed: AISummary = {...aiSummaryFixtures.postCall.plainText, timestamp: 3002}; + task.requestPostCallSummary.mockResolvedValueOnce(refreshed); + + await storeWrapper.requestPostCallSummary(task); + + expect(entryFor('post-call')).toMatchObject({ + status: 'ready', + response: {numberOfTimesCopied: 0, numberOfTimesEdited: 0, feedback: 'none'}, + }); + expect(entryFor('post-call').content).toBe(refreshed); + }); + }); + + describe('feedback', () => { + it('should send the receiving agent feedback with SDK text and keep it once the SDK confirms it', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + receivedListenerOf(task)(aiSummaryFixtures.receivingMidCall.adaptiveCard); + const send = deferred(); + task.sendMidCallSummaryResponse.mockReturnValue(send.promise); + + const feedback = storeWrapper.setReceiverSummaryFeedback( + 'thumbs_up', + aiSummaryFixtures.receivingMidCall.adaptiveCard, + task + ); + + expect(entryFor('receiver')).toMatchObject({feedbackPending: true, response: {feedback: 'none'}}); + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({ + summary: aiSummaryFixtures.receivingMidCall.adaptiveCard.summaryText, + feedback: 'thumbs_up', + }), + 'TRANSFER' + ); + + const current = entryFor('receiver'); + storeWrapper['store'].cc.aiSummaries[INTERACTION_ID].receiver = { + ...current, + response: {...current.response, summary: 'Caller-owned text', numberOfTimesCopied: 1}, + }; + send.resolve(); + await feedback; + expect(entryFor('receiver')).toMatchObject({ + feedbackPending: false, + response: {feedback: 'thumbs_up', summary: 'Caller-owned text', numberOfTimesCopied: 1}, + }); + }); + + it('should send empty text when the receiver SDK payload contains only a card', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + const payload: AISummary = { + conversationId: INTERACTION_ID, + adaptiveCard: aiSummaryFixtures.receivingMidCall.adaptiveCard.adaptiveCard, + }; + receivedListenerOf(task)(payload); + + await storeWrapper.setReceiverSummaryFeedback('thumbs_up', payload, task); + + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({summary: '', state: 'DEFAULT', numberOfTimesViewed: 1, feedback: 'thumbs_up'}), + 'TRANSFER' + ); + }); + + it('should keep the previous receiver selection when the feedback response fails', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + receivedListenerOf(task)(aiSummaryFixtures.receivingMidCall.adaptiveCard); + await storeWrapper.setReceiverSummaryFeedback( + 'thumbs_up', + aiSummaryFixtures.receivingMidCall.adaptiveCard, + task + ); + task.sendMidCallSummaryResponse.mockRejectedValueOnce(new Error('503')); + + await storeWrapper.setReceiverSummaryFeedback( + 'thumbs_down', + aiSummaryFixtures.receivingMidCall.adaptiveCard, + task + ); + + expect(entryFor('receiver')).toMatchObject({feedbackPending: false, response: {feedback: 'thumbs_up'}}); + }); + + it('should ignore overlapping or stale receiver feedback', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + receivedListenerOf(task)(aiSummaryFixtures.receivingMidCall.adaptiveCard); + task.sendMidCallSummaryResponse.mockReturnValue(deferred().promise); + + await storeWrapper.setReceiverSummaryFeedback( + 'thumbs_up', + {...aiSummaryFixtures.receivingMidCall.adaptiveCard}, + task + ); + expect(task.sendMidCallSummaryResponse).not.toHaveBeenCalled(); + + void storeWrapper.setReceiverSummaryFeedback( + 'thumbs_up', + aiSummaryFixtures.receivingMidCall.adaptiveCard, + task + ); + await storeWrapper.setReceiverSummaryFeedback( + 'thumbs_up', + aiSummaryFixtures.receivingMidCall.adaptiveCard, + task + ); + + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledTimes(1); + }); + + it('should not apply confirmed receiver feedback to a summary that replaced it in flight', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + const original = aiSummaryFixtures.receivingMidCall.adaptiveCard; + receivedListenerOf(task)(original); + const send = deferred(); + task.sendMidCallSummaryResponse.mockReturnValue(send.promise); + + const feedback = storeWrapper.setReceiverSummaryFeedback('thumbs_up', original, task); + const replacement: AISummary = {...original, summaryText: 'A newly received summary'}; + receivedListenerOf(task)(replacement); + send.resolve(); + await feedback; + + expect(entryFor('receiver').content).toBe(replacement); + expect(entryFor('receiver')).toMatchObject({response: {feedback: 'none'}}); + expect(entryFor('receiver').feedbackPending).toBeFalsy(); + }); + + it('should not recreate a removed interaction when receiver feedback completes', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + const original = aiSummaryFixtures.receivingMidCall.adaptiveCard; + receivedListenerOf(task)(original); + const send = deferred(); + task.sendMidCallSummaryResponse.mockReturnValue(send.promise); + const feedback = storeWrapper.setReceiverSummaryFeedback('thumbs_up', original, task); + + storeWrapper.handleTaskRemove(task); + send.resolve(); + await feedback; + + expect(storeWrapper.cc.aiSummaries[INTERACTION_ID]).toBeUndefined(); + }); + + it('should not apply old feedback after the interaction is removed and recreated with a new payload', async () => { + const task = makeAISummaryTask(); + registerTaskEventListeners(task); + const original = aiSummaryFixtures.receivingMidCall.adaptiveCard; + receivedListenerOf(task)(original); + const send = deferred(); + task.sendMidCallSummaryResponse.mockReturnValue(send.promise); + const feedback = storeWrapper.setReceiverSummaryFeedback('thumbs_up', original, task); + + storeWrapper.handleTaskRemove(task); + const replacementTask = makeAISummaryTask(); + registerTaskEventListeners(replacementTask); + const replacement: AISummary = {...original, summaryText: 'Summary for the recreated interaction'}; + receivedListenerOf(replacementTask)(replacement); + send.resolve(); + await feedback; + + expect(entryFor('receiver').content).toBe(replacement); + expect(entryFor('receiver')).toMatchObject({response: {feedback: 'none'}}); + expect(entryFor('receiver').feedbackPending).toBeFalsy(); + expect(replacementTask.sendMidCallSummaryResponse).not.toHaveBeenCalled(); + }); + }); + + describe('sendMidCallSummaryResponse', () => { + it('should report the shown initiator summary for the consult or transfer that is starting', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestMidCallSummary('CONSULT', task); + + await expect(storeWrapper.sendMidCallSummaryResponse('TRANSFER', task)).resolves.toBe('submitted'); + + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({state: 'DEFAULT', numberOfTimesViewed: 1}), + 'TRANSFER' + ); + }); + + it('should report a failed summary as not received and a pending one as ignored', async () => { + const task = makeAISummaryTask(); + const reply = deferred(); + task.requestMidCallSummary.mockReturnValueOnce(reply.promise); + const request = storeWrapper.requestMidCallSummary('CONSULT', task); + + await storeWrapper.sendMidCallSummaryResponse('CONSULT', task); + reply.reject(summaryError('MID_CALL_SUMMARY_TIMEOUT')); + await request; + await storeWrapper.sendMidCallSummaryResponse('CONSULT', task); + + expect(task.sendMidCallSummaryResponse).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({summary: '', state: 'IGNORED', numberOfTimesViewed: 0}), + 'CONSULT' + ); + expect(task.sendMidCallSummaryResponse).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({summary: '', state: 'NOT_RECEIVED', numberOfTimesViewed: 0}), + 'CONSULT' + ); + }); + + it('should send nothing when mid-call summaries are disabled for the task', async () => { + const task = makeAISummaryTask({}, {midCallEnabled: false, postCallEnabled: true}); + + await expect(storeWrapper.sendMidCallSummaryResponse('CONSULT', task)).resolves.toBe('not-applicable'); + + expect(task.sendMidCallSummaryResponse).not.toHaveBeenCalled(); + }); + + it('should report a retry without received content as ignored until it settles', async () => { + const task = makeAISummaryTask(); + task.requestMidCallSummary.mockRejectedValueOnce(summaryError('MID_CALL_SUMMARY_TIMEOUT')); + await storeWrapper.requestMidCallSummary('CONSULT', task); + expect(entryFor('initiator').response.state).toBe('NOT_RECEIVED'); + + const reply = deferred(); + task.requestMidCallSummary.mockReturnValueOnce(reply.promise); + const request = storeWrapper.requestMidCallSummary('CONSULT', task); + await storeWrapper.sendMidCallSummaryResponse('CONSULT', task); + + expect(task.sendMidCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({state: 'IGNORED', numberOfTimesViewed: 0}), + 'CONSULT' + ); + + reply.resolve(aiSummaryFixtures.initiatingMidCall.plainText); + await request; + expect(entryFor('initiator').response.state).toBe('DEFAULT'); + }); + + it('should return a failed outcome without rejecting so the action can continue', async () => { + const task = makeAISummaryTask(); + task.sendMidCallSummaryResponse.mockRejectedValueOnce(new Error('503')); + + await expect(storeWrapper.sendMidCallSummaryResponse('CONSULT', task)).resolves.toBe('response-failed'); + }); + }); + + describe('post-call submission', () => { + it('should add the wrap-up code and send the SDK response before task wrap-up', async () => { + const task = makeAISummaryTask(); + + await storeWrapper.requestPostCallSummary(task); + const response: AISummaryResponse = { + summary: {nextSteps: '', additionalContext: 'Caller-edited context'}, + feedback: 'thumbs_down', + state: 'DEFAULT', + numberOfTimesViewed: 2, + numberOfTimesEdited: 1, + numberOfTimesCopied: 3, + }; + entryFor('post-call').response = response; + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', task)).resolves.toBe('submitted'); + + expect(entryFor('post-call').response).toEqual({...response, wrapUpCode: 'aux-billing'}); + expect(task.sendPostCallSummaryResponse).toHaveBeenCalledWith({...response, wrapUpCode: 'aux-billing'}); + expect(entryFor('post-call').content).toBe(aiSummaryFixtures.postCall.plainText); + + const disabledTask = makeAISummaryTask({}, {midCallEnabled: true, postCallEnabled: false}); + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', disabledTask)).resolves.toBe( + 'not-applicable' + ); + expect(disabledTask.sendPostCallSummaryResponse).not.toHaveBeenCalled(); + }); + + it('should report a failed final response without retrying it', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestPostCallSummary(task); + task.sendPostCallSummaryResponse.mockRejectedValueOnce(new Error('503')); + + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', task)).resolves.toBe('response-failed'); + + expect(task.sendPostCallSummaryResponse).toHaveBeenCalledTimes(1); + expect(task.sendPostCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({wrapUpCode: 'aux-billing'}) + ); + }); + + it('should update only the post-call response for the submitted task', async () => { + const task = makeAISummaryTask(); + const nextTask = makeAISummaryTask(); + nextTask.data.interactionId = 'next-interaction'; + await storeWrapper.requestPostCallSummary(task); + storeWrapper['store'].currentTask = nextTask; + + await expect(storeWrapper.sendPostCallSummaryResponse('aux-billing', task)).resolves.toBe('submitted'); + + expect(task.sendPostCallSummaryResponse).toHaveBeenCalledWith( + expect.objectContaining({wrapUpCode: 'aux-billing'}) + ); + expect(nextTask.sendPostCallSummaryResponse).not.toHaveBeenCalled(); + }); + }); + + it('should clear only the removed interaction summaries', async () => { + const task = makeAISummaryTask(); + await storeWrapper.requestMidCallSummary('CONSULT', task); + storeWrapper['store'].cc.aiSummaries = { + ...storeWrapper['store'].cc.aiSummaries, + 'other-interaction': {'post-call': entryFor('initiator')}, + }; + + storeWrapper.handleTaskRemove(task); + + expect(Object.keys(storeWrapper.cc.aiSummaries)).toEqual(['other-interaction']); + }); + }); + describe('offerActionErrors', () => { beforeEach(() => { storeWrapper['store'].offerActionErrors = {}; diff --git a/packages/contact-center/task/ai-docs/task-spec.md b/packages/contact-center/task/ai-docs/task-spec.md index dab8910a5..356b545c5 100644 --- a/packages/contact-center/task/ai-docs/task-spec.md +++ b/packages/contact-center/task/ai-docs/task-spec.md @@ -306,6 +306,11 @@ sequenceDiagram alt manual wrap-up A->>C: select wrap-up code C->>H: wrapupCall(reason, auxCodeId) + opt post-call summary enabled + H->>S: sendPostCallSummaryResponse(auxCodeId, currentTask) + S->>SDK: sendPostCallSummaryResponse(response with wrapUpCode) + SDK-->>S: submitted or response-failed + end H->>SDK: currentTask.wrapup({wrapUpReason, auxCodeId}) SDK-->>H: resolved H->>S: setCurrentTask(firstRemaining); setState(ENGAGED) @@ -430,7 +435,7 @@ The six widget shells are siblings that each bind to exactly one hook and one pr - **UC-5 Mute / unmute (CallControl):** Agent clicks mute → `toggleMute()` (gated by `controlVisibility.muteUnmute`) → `currentTask.toggleMute()` → `store.setIsMuted`, `onToggleMute`. Evidence: `src/helper.ts`, `tests/helper.ts` ("should successfully toggle mute…", "rapid toggleMute"). UI flow: shows error-safe revert on failure. - **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → component reads the matching ordered category array from `currentTask.uiControls.consultTransferDestinations` → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Evidence: `src/helper.ts`, `src/CallControl/index.tsx`, `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx`. - **UC-7 Blind transfer (CallControl):** Agent picks destination → `transferCall(to,type)` → `currentTask.transfer(...)`; failure re-thrown to the modal. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call transferCall successfully"). -- **UC-8 Wrap up a call (CallControl):** Call ends → wrap-up codes shown → Agent selects code → `wrapupCall(reason, auxCodeId)` → `currentTask.wrapup` → next task promoted to `currentTask`, agent → ENGAGED. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call wrapupCall"). Auto-wrap-up: countdown shown with Cancel. UI flow: wrap-up dropdown + optional auto-wrap-up countdown. +- **UC-8 Wrap up a call (CallControl):** Call ends → wrap-up codes shown → Agent selects code → when enabled, await `store.sendPostCallSummaryResponse(auxCodeId, currentTask)` with that exact task and selected code → `currentTask.wrapup({wrapUpReason, auxCodeId})` → next task promoted to `currentTask`, agent → ENGAGED. The disabled capability skips summary submission; both `response-failed` and unexpected summary submission rejections allow wrap-up to continue. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call wrapupCall" and "post-call summary submission during wrap-up", including deferred success ordering, both failure paths, and disabled capability). Auto-wrap-up: countdown shown with Cancel. UI flow: wrap-up dropdown + optional auto-wrap-up countdown. - **UC-9 Place an outbound call (OutdialCall):** Agent enters number, selects ANI → dial → `startOutdial(destination, origin)`; empty number alerts and aborts; disabled while a telephony task is active. Evidence: `src/helper.ts` (`useOutdialCall`), `tests/OutdialCall/index.tsx`. UI flow: dialpad with validation, ANI dropdown, address book toggled by `isAddressBookEnabled`. - **UC-10 Manage concurrent tasks (TaskList):** Agent sees all tasks → accept/decline per task or click to select → selection sets `currentTask` so CallControl follows. Evidence: `src/helper.ts` (`useTaskList`), `tests/helper.ts` (task-list cases), `tests/TaskList/index.tsx`. - **UC-11 View live transcript (RealTimeTranscript):** As `store.realtimeTranscriptionData` updates for `currentTask`, lines are mapped to entries with normalized speaker/time. Evidence: `src/helper.ts` (`useRealTimeTranscript`), `tests/RealtimeTranscript/index.tsx`. diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 3fb28e279..8d7a57b1f 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1240,26 +1240,35 @@ export const useCallControl = (props: useCallControlProps) => { } }; - const wrapupCall = (wrapUpReason: string, auxCodeId: string) => { + const wrapupCall = async (wrapUpReason: string, auxCodeId: string): Promise => { try { // Store auxCodeId for use in wrapupCallCallback lastWrapupAuxCodeIdRef.current = auxCodeId; - currentTask - .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) - .then(() => { - const taskKeys = Object.keys(store.taskList); - if (taskKeys.length > 0) { - store.setCurrentTask(store.taskList[taskKeys[0]]); - store.setState({ - developerName: ENGAGED_LABEL, - name: ENGAGED_USERNAME, - }); - } - }) - .catch((error: Error) => { - logError(`Error wrapping up call: ${error}`, 'wrapupCall'); - }); + if (currentTask.aiSummaryCapabilities?.postCallEnabled) { + try { + await store.sendPostCallSummaryResponse(auxCodeId, currentTask); + } catch (error) { + logger?.error(`CC-Widgets: AI summary response failed: ${error}`, { + module: 'useCallControl', + method: 'wrapupCall', + }); + } + } + + try { + await currentTask.wrapup({wrapUpReason, auxCodeId}); + const taskKeys = Object.keys(store.taskList); + if (taskKeys.length > 0) { + store.setCurrentTask(store.taskList[taskKeys[0]]); + store.setState({ + developerName: ENGAGED_LABEL, + name: ENGAGED_USERNAME, + }); + } + } catch (error) { + logError(`Error wrapping up call: ${error}`, 'wrapupCall'); + } } catch (error) { logger?.error(`CC-Widgets: Task: Error in wrapupCall - ${error.message}`, { module: 'useCallControl', diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index a4a2bec0a..6d0a4eaba 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -2924,6 +2924,124 @@ describe('useCallControl', () => { }); }); + describe('post-call summary submission during wrap-up', () => { + const createPostCallTask = () => ({ + ...mockCurrentTask, + aiSummaryCapabilities: {midCallEnabled: false, postCallEnabled: true}, + wrapup: jest.fn().mockResolvedValue(undefined), + }); + + it('should await post-call summary submission with the selected code and task before wrapping up', async () => { + const task = createPostCallTask(); + let resolveSubmission!: (result: 'submitted') => void; + const submissionPromise = new Promise<'submitted'>((resolve) => { + resolveSubmission = resolve; + }); + const sendSummaryResponse = jest.spyOn(store, 'sendPostCallSummaryResponse').mockReturnValue(submissionPromise); + const {result} = renderHook(() => + useCallControl({ + currentTask: task, + logger: mockLogger, + isMuted: false, + conferenceEnabled: true, + agentId: 'test-agent-id', + }) + ); + + let wrapupPromise!: Promise; + act(() => { + wrapupPromise = result.current.wrapupCall('Wrap reason', '123'); + }); + + expect(sendSummaryResponse).toHaveBeenCalledTimes(1); + expect(sendSummaryResponse).toHaveBeenCalledWith('123', task); + expect(task.wrapup).not.toHaveBeenCalled(); + + await act(async () => { + resolveSubmission('submitted'); + await wrapupPromise; + }); + + expect(task.wrapup).toHaveBeenCalledTimes(1); + expect(task.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: '123'}); + }); + + it('should continue wrapping up when post-call summary submission returns response-failed', async () => { + const task = createPostCallTask(); + const sendSummaryResponse = jest.spyOn(store, 'sendPostCallSummaryResponse').mockResolvedValue('response-failed'); + const {result} = renderHook(() => + useCallControl({ + currentTask: task, + logger: mockLogger, + isMuted: false, + conferenceEnabled: true, + agentId: 'test-agent-id', + }) + ); + + await act(async () => { + await result.current.wrapupCall('Wrap reason', '123'); + }); + + expect(sendSummaryResponse).toHaveBeenCalledWith('123', task); + expect(task.wrapup).toHaveBeenCalledTimes(1); + expect(task.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: '123'}); + }); + + it('should log unexpected post-call summary rejection and continue wrapping up', async () => { + const task = createPostCallTask(); + const sendSummaryResponse = jest + .spyOn(store, 'sendPostCallSummaryResponse') + .mockRejectedValue(new Error('Unexpected summary submission error')); + const {result} = renderHook(() => + useCallControl({ + currentTask: task, + logger: mockLogger, + isMuted: false, + conferenceEnabled: true, + agentId: 'test-agent-id', + }) + ); + + await act(async () => { + await result.current.wrapupCall('Wrap reason', '123'); + }); + + expect(sendSummaryResponse).toHaveBeenCalledWith('123', task); + expect(mockLogger.error).toHaveBeenCalledWith( + 'CC-Widgets: AI summary response failed: Error: Unexpected summary submission error', + {module: 'useCallControl', method: 'wrapupCall'} + ); + expect(task.wrapup).toHaveBeenCalledTimes(1); + expect(task.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: '123'}); + }); + + it('should skip post-call summary submission when the capability is disabled', async () => { + const task = { + ...createPostCallTask(), + aiSummaryCapabilities: {midCallEnabled: false, postCallEnabled: false}, + }; + const sendSummaryResponse = jest.spyOn(store, 'sendPostCallSummaryResponse'); + const {result} = renderHook(() => + useCallControl({ + currentTask: task, + logger: mockLogger, + isMuted: false, + conferenceEnabled: true, + agentId: 'test-agent-id', + }) + ); + + await act(async () => { + await result.current.wrapupCall('Wrap reason', '123'); + }); + + expect(sendSummaryResponse).not.toHaveBeenCalled(); + expect(task.wrapup).toHaveBeenCalledTimes(1); + expect(task.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: '123'}); + }); + }); + it('should log an error if wrapup fails', async () => { mockCurrentTask.wrapup.mockRejectedValueOnce(new Error('Wrapup error')); @@ -7961,7 +8079,7 @@ describe('Task Hook Error Handling and Logging', () => { }); }); - it('should handle synchronous errors in wrapupCall', () => { + it('should handle synchronous errors in wrapupCall', async () => { const errorTask = { ...mockTaskWithInteraction, wrapup: jest.fn().mockImplementation(() => { @@ -7979,17 +8097,14 @@ describe('Task Hook Error Handling and Logging', () => { }) ); - act(() => { - result.current.wrapupCall('test reason', '123'); + await act(async () => { + await result.current.wrapupCall('test reason', '123'); }); - expect(logger.error).toHaveBeenCalledWith( - 'CC-Widgets: Task: Error in wrapupCall - wrapupCall synchronous error', - { - module: 'useCallControl', - method: 'wrapupCall', - } - ); + expect(logger.error).toHaveBeenCalledWith('Error wrapping up call: Error: wrapupCall synchronous error', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#wrapupCall', + }); }); it('should handle errors in getAddressBookEntries', async () => { diff --git a/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md index 47c12808a..d8f2b749c 100644 --- a/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md +++ b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md @@ -34,6 +34,8 @@ The package is structured as a flat set of fixture modules under `src/`, each on The load-bearing contract of this module is structural: each exported mock is typed against the real SDK / store / cc-components type (e.g. `mockCC: IContactCenter`, `mockProfile: Profile`, `mockTask: ITask`) so that when a consuming test passes a fixture into production code, the shape matches what production code expects at compile time. A maintainer changing a fixture should start at `src/fixtures.ts` and keep the declared types intact. +`mockTask` and `makeMockTask` use the store's exported `ITask` contract. They provide disabled AI summary capabilities and stub request/response methods, so store consumers see the same task shape in tests. + ## Purpose / Responsibility Provides shared, type-checked mock SDK instances, mock profile/task/queue/agent data, and component-prop fixtures consumed by other packages' Jest tests; it does NOT provide runtime behavior, test runners, or shared test-render helpers. @@ -122,6 +124,9 @@ Shape fidelity is achieved by importing the real SDK/store types and annotating Isolation is offered two ways. Static fixtures (`mockTask`, `mockCC`) are shared singletons — cheap but mutable, so tests that mutate must clone. Factory functions (`makeMockTask`, `makeMockCampaignTask`, `makeMockAddressBook`) return fresh objects with brand-new `jest.fn()`s each call, which is the safe path for tests that mutate or assert call counts. +`mockCC.aiSummaries` starts empty to match the widget-owned state on `IContactCenter`; tests that +populate it must reset or replace it to keep summary state isolated. + Composition keeps the SDK mock single-sourced: outdial fixtures spread `mockCC` rather than redeclaring it, so a change to `mockCC` propagates. The same principle is why `mockQueuesResponse` is derived by mapping `mockQueueDetails` instead of being hand-written twice. ## Data Flow @@ -227,6 +232,19 @@ The package ships no tests of its own (no `tests/` directory; confirmed by tree) | `test-fixtures-R-007` (barrel surface) | None found | No test guarding the public export list | | `test-fixtures-R-008` (task UI controls) | Call-control consumer tests; `@webex/test-fixtures` and `@webex/cc-components` builds | No package-local assertion for destination override merging | +## AI summary fixtures + +`src/aiSummaryFixtures.ts` exports inputs checked against the SDK summary type through +`AISummaryEntry['content']` from the store. They are used by the store's widget-integration +tests: initiating mid-call sections/plain text, receiving-agent Adaptive Cards with +SDK summary text distinct from the displayed card, unsupported content, and post-call +content with Outcome boundaries. The receiver text verifies that feedback preserves +the SDK payload rather than reconstructing it from card labels. The barrel +re-exports `aiSummaryFixtures`. SDK transport, request ordering and package admission +fixtures belong to the SDK repository. + +Consumers: the AI summary and lifecycle selector cases in `store/tests/storeEventsWrapper.ts`. + ## Traceability - Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) - Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/test-fixtures/src/aiSummaryFixtures.ts b/packages/contact-center/test-fixtures/src/aiSummaryFixtures.ts new file mode 100644 index 000000000..c60e3752b --- /dev/null +++ b/packages/contact-center/test-fixtures/src/aiSummaryFixtures.ts @@ -0,0 +1,141 @@ +import type {AISummaryEntry} from '@webex/cc-store'; + +type AISummary = NonNullable; + +const MAIN_INTERACTION_ID = 'interaction-main-1'; + +const initiatingMidCall = { + typedSections: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 2000, + languageCode: 'en-US', + areTranscriptsAvailable: true, + sections: { + reasonForTransferOrConsult: 'Customer needs billing help.', + additionalContext: 'Invoice discrepancy is the active topic.', + keyActionsTaken: 'Verified account and checked invoice.', + }, + summaryText: 'Customer needs billing help. Verified account and checked invoice.', + }, + plainText: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 2001, + languageCode: 'en-US', + areTranscriptsAvailable: true, + summaryText: 'Customer needs billing help. Verified account and checked invoice.', + }, + cardOnlyUnsupported: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 2002, + languageCode: 'en-US', + areTranscriptsAvailable: true, + adaptiveCardId: 'mid-call-initiator-card-only', + adaptiveCard: { + type: 'AdaptiveCard', + version: '1.5', + body: [{type: 'TextBlock', text: 'Card-only initiator summary'}], + }, + }, +} as const satisfies Readonly>; + +const receivingMidCall = { + adaptiveCard: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 2100, + languageCode: 'en-US', + areTranscriptsAvailable: true, + summaryText: 'Transferred billing inquiry: invoice correction needed.', + sections: { + reasonForTransferOrConsult: 'Receiver typed sections must be ignored.', + additionalContext: 'Receiver additional context must be ignored.', + keyActionsTaken: 'Receiver actions must be ignored.', + }, + adaptiveCardId: 'mid-call-receiver-card', + adaptiveCard: { + type: 'AdaptiveCard', + version: '1.5', + body: [ + {type: 'TextBlock', text: 'Customer needs billing help.'}, + {type: 'TextBlock', text: 'Invoice discrepancy is the active topic.'}, + ], + }, + }, + typedOnlyUnsupported: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 2103, + languageCode: 'en-US', + sections: { + reasonForTransferOrConsult: 'Typed receiver payload.', + }, + }, +} as const satisfies Readonly>; + +const postCall = { + structured: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3000, + languageCode: 'en-US', + areTranscriptsAvailable: true, + sections: { + initialContactReason: 'Customer called about an invoice discrepancy.', + additionalContactReasons: 'Customer also asked about late fees.', + additionalContext: 'Customer is positive about the correction.', + keyActionsTaken: 'Send corrected invoice by email.', + nextSteps: 'Confirm receipt tomorrow.', + }, + summaryText: 'Customer called about an invoice discrepancy. Send corrected invoice by email.', + resolution: 'Billing adjustment prepared', + }, + plainText: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3001, + languageCode: 'en-US', + areTranscriptsAvailable: true, + summaryText: 'Customer called about an invoice discrepancy. Send corrected invoice by email.', + }, + cardOnlyUnsupported: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3002, + languageCode: 'en-US', + areTranscriptsAvailable: true, + adaptiveCardId: 'post-call-card-only', + adaptiveCard: { + type: 'AdaptiveCard', + version: '1.5', + body: [{type: 'TextBlock', text: 'Card-only post-call summary'}], + }, + }, + resolutionPresent: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3003, + languageCode: 'en-US', + sections: { + initialContactReason: 'Customer asked for an invoice correction.', + nextSteps: 'Email the corrected invoice.', + }, + summaryText: 'Customer asked for an invoice correction. Email the corrected invoice.', + resolution: 'Correction approved', + }, + resolutionAbsent: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3003, + languageCode: 'en-US', + sections: { + initialContactReason: 'Customer asked for an invoice correction.', + nextSteps: 'Email the corrected invoice.', + }, + summaryText: 'Customer asked for an invoice correction. Email the corrected invoice.', + }, + resolutionEmptyBoundary: { + conversationId: MAIN_INTERACTION_ID, + timestamp: 3005, + languageCode: 'en-US', + sections: { + initialContactReason: 'Boundary fixture.', + }, + summaryText: 'Boundary fixture.', + resolution: '', + }, +} as const satisfies Readonly>; + +export const aiSummaryFixtures = {initiatingMidCall, receivingMidCall, postCall} as const; diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 949db0614..6e3103583 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -1,5 +1,4 @@ import type { - ITask, Interaction, Profile, TaskData, @@ -9,7 +8,7 @@ import type { AddressBookEntriesResponse, ContactServiceQueuesResponse, } from '@webex/contact-center'; -import {IContactCenter} from '@webex/cc-store'; +import type {IContactCenter, ITask} from '@webex/cc-store'; const mockProfile: Profile = { teams: [{teamId: 'team1', teamName: 'Team 1'}], @@ -166,6 +165,11 @@ const mockTask = { } as unknown as TaskData, webCallMap: {}, autoWrapup: undefined, + aiSummaryCapabilities: {midCallEnabled: false, postCallEnabled: false}, + requestMidCallSummary: jest.fn().mockResolvedValue({}), + requestPostCallSummary: jest.fn().mockResolvedValue({}), + sendMidCallSummaryResponse: jest.fn().mockResolvedValue(undefined), + sendPostCallSummaryResponse: jest.fn().mockResolvedValue(undefined), on: jest.fn(), off: jest.fn(), emit: jest.fn(), @@ -222,6 +226,10 @@ const makeMockTask = (overrides?: MakeMockTaskOverrides): ITask => { ...interactionOverrides, }, } as unknown as TaskData, + requestMidCallSummary: jest.fn().mockResolvedValue({}), + requestPostCallSummary: jest.fn().mockResolvedValue({}), + sendMidCallSummaryResponse: jest.fn().mockResolvedValue(undefined), + sendPostCallSummaryResponse: jest.fn().mockResolvedValue(undefined), on: jest.fn(), off: jest.fn(), once: jest.fn(), @@ -600,6 +608,7 @@ const mockQueuesResponse: ContactServiceQueuesResponse = { }; const mockCC: IContactCenter = { + aiSummaries: {}, on: jest.fn(), off: jest.fn(), updateAgentProfile: jest.fn(), diff --git a/packages/contact-center/test-fixtures/src/index.ts b/packages/contact-center/test-fixtures/src/index.ts index b8fa32a20..5dcfad45b 100644 --- a/packages/contact-center/test-fixtures/src/index.ts +++ b/packages/contact-center/test-fixtures/src/index.ts @@ -1,4 +1,5 @@ export * from './fixtures'; +export {aiSummaryFixtures} from './aiSummaryFixtures'; export * from './incomingTaskFixtures'; export * from './taskListFixtures'; export * from './taskUIControlsFixtures'; diff --git a/yarn.lock b/yarn.lock index cd92c0bb4..046b8b613 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9584,6 +9584,30 @@ __metadata: languageName: node linkType: hard +"@webex/calling@npm:3.12.0-next.113": + version: 3.12.0-next.113 + resolution: "@webex/calling@npm:3.12.0-next.113" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/internal-media-core": "npm:2.31.0" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/internal-plugin-feature": "npm:3.12.0-next.57" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.56" + "@webex/media-helpers": "npm:3.12.0-next.17" + async-mutex: "npm:0.4.0" + backoff: "npm:2.5.0" + buffer: "npm:6.0.3" + lodash: "npm:^4.17.21" + platform: "npm:1.3.6" + uuid: "npm:8.3.2" + ws: "npm:8.17.1" + xstate: "npm:4.30.6" + checksum: 10c0/3f01242eb718d6f1240f4668231991793297e2036fe8ec7c7b3748969e57f9e8423b8ae56a93fdd79a8c72e391c9fc82209fa03b881966715028cef1504e0966 + languageName: node + linkType: hard + "@webex/cc-ai-assistant@workspace:*, @webex/cc-ai-assistant@workspace:packages/contact-center/ai-assistant": version: 0.0.0-use.local resolution: "@webex/cc-ai-assistant@workspace:packages/contact-center/ai-assistant" @@ -9792,7 +9816,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" - "@webex/contact-center": "npm:3.12.0-next.133" + "@webex/contact-center": "npm:3.12.0-next.148" "@webex/event-dictionary-ts": "npm:^1.0.2191" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" @@ -10019,6 +10043,13 @@ __metadata: languageName: node linkType: hard +"@webex/common-timers@npm:3.12.0-next.10": + version: 3.12.0-next.10 + resolution: "@webex/common-timers@npm:3.12.0-next.10" + checksum: 10c0/cc5ce4b5311617fd6ee280297582b9c0ed6c0d07450bcbcc821eb96636e4664282848bdf33e1eb24ae4296a1f15eaba9ce995d692f3e932fe748996217facff1 + languageName: node + linkType: hard + "@webex/common-timers@npm:3.12.0-next.7": version: 3.12.0-next.7 resolution: "@webex/common-timers@npm:3.12.0-next.7" @@ -10089,6 +10120,21 @@ __metadata: languageName: node linkType: hard +"@webex/common@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/common@npm:3.12.0-next.9" + dependencies: + backoff: "npm:^2.5.0" + bowser: "npm:^2.11.0" + core-decorators: "npm:^0.20.0" + global: "npm:^4.4.0" + lodash: "npm:^4.17.21" + safe-buffer: "npm:^5.2.0" + urlsafe-base64: "npm:^1.0.0" + checksum: 10c0/81635e5904a856de3d4f0e2135513bd7cc1e66cae103b0bfdb67a6b04ceb27208197f6263d5bacda56696d464a9fabd4564f60ccdabb58de9b57ff940e388451 + languageName: node + linkType: hard + "@webex/component-adapter-interfaces@npm:^1.28.0, @webex/component-adapter-interfaces@npm:^1.30.5": version: 1.30.18 resolution: "@webex/component-adapter-interfaces@npm:1.30.18" @@ -10145,6 +10191,26 @@ __metadata: languageName: node linkType: hard +"@webex/contact-center@npm:3.12.0-next.148": + version: 3.12.0-next.148 + resolution: "@webex/contact-center@npm:3.12.0-next.148" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/calling": "npm:3.12.0-next.113" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.61" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.55" + "@webex/internal-plugin-support": "npm:3.12.0-next.69" + "@webex/plugin-authorization": "npm:3.12.0-next.57" + "@webex/plugin-logger": "npm:3.12.0-next.56" + "@webex/webex-core": "npm:3.12.0-next.57" + jest-html-reporters: "npm:3.0.11" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + xstate: "npm:5.24.0" + checksum: 10c0/83be552140a91441abf441eaf3efbb7c1279c522096dc6cdf77227611ab20f3a8c59b6211026d986cd837b0207e16291e85c4d9a8c34906d980cd77ab71dd949 + languageName: node + linkType: hard + "@webex/event-dictionary-ts@npm:^1.0.2191": version: 1.0.2204 resolution: "@webex/event-dictionary-ts@npm:1.0.2204" @@ -10202,6 +10268,15 @@ __metadata: languageName: node linkType: hard +"@webex/helper-html@npm:3.12.0-next.10": + version: 3.12.0-next.10 + resolution: "@webex/helper-html@npm:3.12.0-next.10" + dependencies: + lodash: "npm:^4.17.21" + checksum: 10c0/98519a5669e4538cc2190b60a0d8037f2482b3f2dbee1f452b2892da61b0a7966b90d60a7503d1a256f191dbb80ec6c277d48ea094fd5fe67c4bd07908f1a774 + languageName: node + linkType: hard + "@webex/helper-html@npm:3.12.0-next.8": version: 3.12.0-next.8 resolution: "@webex/helper-html@npm:3.12.0-next.8" @@ -10262,6 +10337,23 @@ __metadata: languageName: node linkType: hard +"@webex/helper-image@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/helper-image@npm:3.12.0-next.9" + dependencies: + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-file": "npm:3.12.0-next.9" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + exifr: "npm:^5.0.3" + gm: "npm:^1.23.1" + lodash: "npm:^4.17.21" + mime: "npm:^2.4.4" + safe-buffer: "npm:^5.2.0" + checksum: 10c0/9e1069d3057be089003d33d83b4f3d7dfc79a1c359348badf43369ab4b62f0d01a53feed42cef9062092174bc75608c622a018fdcbf4f884d5437dafb9f9e52e + languageName: node + linkType: hard + "@webex/http-core@npm:1.161.0": version: 1.161.0 resolution: "@webex/http-core@npm:1.161.0" @@ -10339,6 +10431,24 @@ __metadata: languageName: node linkType: hard +"@webex/http-core@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/http-core@npm:3.12.0-next.9" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + file-type: "npm:^16.0.1" + global: "npm:^4.4.0" + is-function: "npm:^1.0.1" + lodash: "npm:^4.17.21" + parse-headers: "npm:^2.0.2" + qs: "npm:^6.7.3" + request: "npm:^2.88.0" + safe-buffer: "npm:^5.2.0" + xtend: "npm:^4.0.2" + checksum: 10c0/7ab24cbde6e03c43a326832c9a849422f2cd5b896d7942c6bd532f4593467fb80eb4fc3b29c7e59d92ca8ef99f5e4a8d6237e4d6ee6f80cf9d15ce25a24f593d + languageName: node + linkType: hard + "@webex/internal-media-core@npm:0.0.7-beta": version: 0.0.7-beta resolution: "@webex/internal-media-core@npm:0.0.7-beta" @@ -10376,6 +10486,26 @@ __metadata: languageName: node linkType: hard +"@webex/internal-media-core@npm:2.31.0": + version: 2.31.0 + resolution: "@webex/internal-media-core@npm:2.31.0" + dependencies: + "@babel/runtime": "npm:^7.18.9" + "@babel/runtime-corejs2": "npm:^7.25.0" + "@webex/rtcstats": "npm:^1.5.5" + "@webex/ts-sdp": "npm:1.8.2" + "@webex/web-capabilities": "npm:^1.13.1" + "@webex/web-client-media-engine": "npm:3.47.1" + events: "npm:^3.3.0" + ip-anonymize: "npm:^0.1.0" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + webrtc-adapter: "npm:^8.1.2" + xstate: "npm:^4.30.6" + checksum: 10c0/67a784cdc2d8a21965e94fb4060b0008e01761c155fd8c846e747ed9b9ce69c070035488f123f8f51f8f6a199d590ecb0480798bc5c216af338486a77b5dd298 + languageName: node + linkType: hard + "@webex/internal-plugin-calendar@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-calendar@npm:2.60.4" @@ -10458,6 +10588,24 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-conversation@npm:3.12.0-next.69": + version: 3.12.0-next.69 + resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.69" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/helper-html": "npm:3.12.0-next.10" + "@webex/helper-image": "npm:3.12.0-next.9" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.65" + "@webex/internal-plugin-user": "npm:3.12.0-next.58" + "@webex/webex-core": "npm:3.12.0-next.57" + crypto-js: "npm:^4.1.1" + lodash: "npm:^4.17.21" + node-scr: "npm:^0.3.0" + uuid: "npm:^3.3.2" + checksum: 10c0/2b5a3de63ffc60bf608f9efecbb2d2ca683f4023dfb6f913231c40e56c1c1c7c255f238e836be5b4ae97d0e8702ee5f4da17da54d5d004bcb49268d051bdbc95 + languageName: node + linkType: hard + "@webex/internal-plugin-device@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-device@npm:2.60.4" @@ -10508,6 +10656,40 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-device@npm:3.12.0-next.54": + version: 3.12.0-next.54 + resolution: "@webex/internal-plugin-device@npm:3.12.0-next.54" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.54" + "@webex/webex-core": "npm:3.12.0-next.54" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/bd070078aa6943bb736474eaebc2fe5a842fec8f5b3e648e908a4308503f45438dadd10b7739cd0bfc1fd514a8210b55083f17c90acb6d71b86d011c48985301 + languageName: node + linkType: hard + +"@webex/internal-plugin-device@npm:3.12.0-next.55": + version: 3.12.0-next.55 + resolution: "@webex/internal-plugin-device@npm:3.12.0-next.55" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.56" + "@webex/webex-core": "npm:3.12.0-next.60" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/5ea16f4b1fa2f4be789dfb63e84e8b7d121313a5df04099ce0b72102f59b5032a2fe51d359d0ba9974c5c28e4f4433d0c6334f7ffcd7fc694e83eea7afbe351a + languageName: node + linkType: hard + "@webex/internal-plugin-dss@npm:3.12.0-next.53": version: 3.12.0-next.53 resolution: "@webex/internal-plugin-dss@npm:3.12.0-next.53" @@ -10600,6 +10782,32 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-encryption@npm:3.12.0-next.65": + version: 3.12.0-next.65 + resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.65" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/internal-plugin-device": "npm:3.12.0-next.54" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.62" + "@webex/test-helper-file": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.57" + asn1js: "npm:^2.0.26" + debug: "npm:^4.3.4" + isomorphic-webcrypto: "npm:^2.3.8" + lodash: "npm:^4.17.21" + node-jose: "npm:^2.2.0" + node-kms: "npm:^0.4.1" + node-scr: "npm:^0.3.0" + pkijs: "npm:^2.1.84" + safe-buffer: "npm:^5.2.0" + uuid: "npm:^3.3.2" + valid-url: "npm:^1.0.9" + checksum: 10c0/93596f6a5a63ba9105cb5499f3d80e1dc849568f9826402acfdc64e9ae7328bc592bab4feae46fb48cf46ca53082ec48448ac2d18caf6fdf73651685fb3eb70c + languageName: node + linkType: hard + "@webex/internal-plugin-feature@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-feature@npm:2.60.4" @@ -10634,6 +10842,28 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-feature@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.57" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/webex-core": "npm:3.12.0-next.57" + lodash: "npm:^4.17.21" + checksum: 10c0/db7c2b991d206b39e019af2e466f452dabc7acfe2bb599f6b6a5724fda967391795f80df71395f11614f975c5da0d0d1c21cd7ec0cd82264769598afd168ee5c + languageName: node + linkType: hard + +"@webex/internal-plugin-feature@npm:3.12.0-next.60": + version: 3.12.0-next.60 + resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.60" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/webex-core": "npm:3.12.0-next.60" + lodash: "npm:^4.17.21" + checksum: 10c0/315a926343254f507ff9ee7b2f8527a36555de38eaa03815415557745bad414f45754e2ae0367d69010dbe11b93b445e44676a3c3f190f8c43ef10a0aeaa868a + languageName: node + linkType: hard + "@webex/internal-plugin-llm@npm:3.12.0-next.56": version: 3.12.0-next.56 resolution: "@webex/internal-plugin-llm@npm:3.12.0-next.56" @@ -10777,6 +11007,54 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-mercury@npm:3.12.0-next.61": + version: 3.12.0-next.61 + resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.61" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/internal-plugin-feature": "npm:3.12.0-next.57" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.56" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + "@webex/test-helper-mock-web-socket": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/test-helper-refresh-callback": "npm:3.12.0-next.9" + "@webex/test-helper-test-users": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.57" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/0e7c46aec1074ad658a4e4069c088f7be19152daf51fed70b9ebc8122e1d0053c1457ed7bbb2bac38e10a545f51550ac4aacaf5628f72d240819fba9e32b96da + languageName: node + linkType: hard + +"@webex/internal-plugin-mercury@npm:3.12.0-next.62": + version: 3.12.0-next.62 + resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.62" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/internal-plugin-feature": "npm:3.12.0-next.60" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.56" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + "@webex/test-helper-mock-web-socket": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/test-helper-refresh-callback": "npm:3.12.0-next.9" + "@webex/test-helper-test-users": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.60" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/1470378e5b3862b124ecb8b118d06c5dba56e275f26fc89f274408a33cd3bd38820947c1a3a8fd8486d4d6b2fb6ae239dae04e8368568b9fe7e07b7cc0bf50f3 + languageName: node + linkType: hard + "@webex/internal-plugin-metrics@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-metrics@npm:2.60.4" @@ -10824,6 +11102,60 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-metrics@npm:3.12.0-next.54": + version: 3.12.0-next.54 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.54" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/event-dictionary-ts": "npm:^1.0.2220" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.54" + ip-anonymize: "npm:^0.1.0" + isbot: "npm:5.2.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/1e84f5186a85f5db217e457a6d0d9f028c2391ea1d5bbab8ec09b852e7199c9979eb757b0773afa0a727029a1e33fdf07a44e8c9b28ed392f4d6c0131c5b5dbb + languageName: node + linkType: hard + +"@webex/internal-plugin-metrics@npm:3.12.0-next.55": + version: 3.12.0-next.55 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.55" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/event-dictionary-ts": "npm:^1.0.2220" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.55" + ip-anonymize: "npm:^0.1.0" + isbot: "npm:5.2.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/04944fb40a298cbba5e25431a457943285bfb278131db82190e62efa45aef56cce85abfe1e60b7711f599042221d88568fbbcfa201b02a4731f7b0f468f223ea + languageName: node + linkType: hard + +"@webex/internal-plugin-metrics@npm:3.12.0-next.56": + version: 3.12.0-next.56 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.56" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/event-dictionary-ts": "npm:^1.0.2220" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.60" + ip-anonymize: "npm:^0.1.0" + isbot: "npm:5.2.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/16e099d61b190620043fdb5f40669ca5f50d5128914c7f82c9586b4893158686ae893a3bcb7cee3e84ee22e04a3368aea40c558b11d6003d319e656084157759 + languageName: node + linkType: hard + "@webex/internal-plugin-presence@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-presence@npm:2.60.4" @@ -10886,6 +11218,21 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-search@npm:3.12.0-next.69": + version: 3.12.0-next.69 + resolution: "@webex/internal-plugin-search@npm:3.12.0-next.69" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.69" + "@webex/internal-plugin-device": "npm:3.12.0-next.54" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.65" + "@webex/webex-core": "npm:3.12.0-next.57" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/e5c5699dcbea1fdec00f045207db7ca81e54d0efcfa8fead9d9de4a7ac294b368175ae796ae522e0de7818ac34c1bb0523ccf2ff54030dc6f752676a73ccc6d8 + languageName: node + linkType: hard + "@webex/internal-plugin-support@npm:2.60.4": version: 2.60.4 resolution: "@webex/internal-plugin-support@npm:2.60.4" @@ -10920,6 +11267,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-support@npm:3.12.0-next.69": + version: 3.12.0-next.69 + resolution: "@webex/internal-plugin-support@npm:3.12.0-next.69" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-next.54" + "@webex/internal-plugin-search": "npm:3.12.0-next.69" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-file": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/test-helper-test-users": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.57" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/2abcdadd78e418ba3cc21bc0c30692765a00cd5c0e08c529df20924171915ae3476fdd9ded403ac3c08cf590fc45032f42228ddd874d0c0a411180ff3f449253 + languageName: node + linkType: hard + "@webex/internal-plugin-task@npm:^3.12.0-next.58": version: 3.12.0 resolution: "@webex/internal-plugin-task@npm:3.12.0" @@ -10982,6 +11346,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-user@npm:3.12.0-next.58": + version: 3.12.0-next.58 + resolution: "@webex/internal-plugin-user@npm:3.12.0-next.58" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/test-helper-test-users": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.57" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/b5d9a1805f96668ab53ee1cee533e6f6658df6ccc3075ed0e40d2dbdbe717222d8c8e5ea8c05ed6ace6983afc6d8ec07a43eaca30317d82c371983ede99eae30 + languageName: node + linkType: hard + "@webex/internal-plugin-voicea@npm:3.12.0-next.59": version: 3.12.0-next.59 resolution: "@webex/internal-plugin-voicea@npm:3.12.0-next.59" @@ -11025,6 +11405,17 @@ __metadata: languageName: node linkType: hard +"@webex/media-helpers@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/media-helpers@npm:3.12.0-next.17" + dependencies: + "@webex/internal-media-core": "npm:2.30.1" + "@webex/ts-events": "npm:^1.1.0" + "@webex/web-media-effects": "npm:2.37.0" + checksum: 10c0/02c114ca1776e1e86c7b059e0d13f89138391fb681b0fcb1d851dec3a9a935e1a9781f2f9829bde6f729dd743bba2b0625924a86ddf9048f9ac6e628fc0169f5 + languageName: node + linkType: hard + "@webex/package-tools@npm:0.0.0-next.6": version: 0.0.0-next.6 resolution: "@webex/package-tools@npm:0.0.0-next.6" @@ -11126,6 +11517,23 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-browser@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.57" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/plugin-authorization-node": "npm:3.12.0-next.57" + "@webex/storage-adapter-local-storage": "npm:3.12.0-next.57" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + "@webex/webex-core": "npm:3.12.0-next.57" + jsonwebtoken: "npm:^9.0.2" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/c91d3d31c51c0bd1d2f71d1437bfb753f2a062ebf84c3d58deb6ea752102a39b1ef33aa9a0d517daff91b74d6cb051ae684b2ea13baeecc0241e07586e6364b4 + languageName: node + linkType: hard + "@webex/plugin-authorization-node@npm:2.60.4": version: 2.60.4 resolution: "@webex/plugin-authorization-node@npm:2.60.4" @@ -11152,6 +11560,19 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-node@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.57" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/internal-plugin-device": "npm:3.12.0-next.55" + "@webex/webex-core": "npm:3.12.0-next.57" + jsonwebtoken: "npm:^9.0.0" + uuid: "npm:^3.3.2" + checksum: 10c0/db7b18e414a42de92862838017790b738452946a7dca88eeb0d23bad8590c760cfc893476a45cbe13c06820fc8564a7d196510fc25eca1d943a9d3036687c7bd + languageName: node + linkType: hard + "@webex/plugin-authorization@npm:2.60.4": version: 2.60.4 resolution: "@webex/plugin-authorization@npm:2.60.4" @@ -11172,6 +11593,16 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/plugin-authorization@npm:3.12.0-next.57" + dependencies: + "@webex/plugin-authorization-browser": "npm:3.12.0-next.57" + "@webex/plugin-authorization-node": "npm:3.12.0-next.57" + checksum: 10c0/44ae51deff8560b7d69b150533cca1aa7c423946773244c460595a98768bc1b4194b14bef5c7b4d01272c6c5e672f388cf671c1d80a29dad780cec6800108e73 + languageName: node + linkType: hard + "@webex/plugin-device-manager@npm:2.60.4": version: 2.60.4 resolution: "@webex/plugin-device-manager@npm:2.60.4" @@ -11246,6 +11677,20 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-logger@npm:3.12.0-next.56": + version: 3.12.0-next.56 + resolution: "@webex/plugin-logger@npm:3.12.0-next.56" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/test-helper-chai": "npm:3.12.0-next.9" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.56" + lodash: "npm:^4.17.21" + checksum: 10c0/6a14b867c499fb017563b1d0c71669d7ac0f3e0f3f31dbc86e3ee91a6af9c9c5c21198077c8cfb319f170373d1ddefc4b34eb7b807cbd0b9776e411bb6c79234 + languageName: node + linkType: hard + "@webex/plugin-meetings@npm:2.60.4": version: 2.60.4 resolution: "@webex/plugin-meetings@npm:2.60.4" @@ -11572,6 +12017,17 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-local-storage@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.57" + dependencies: + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + "@webex/webex-core": "npm:3.12.0-next.57" + checksum: 10c0/daa220715ce1a1a146beab6885e23237ee381be5be936d9d75cf5aba54e2c388e06dbc758a3c7db28fe0e3c35eefb61d47640256f8e2056838a93c544bdc52a0 + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:2.60.4": version: 2.60.4 resolution: "@webex/storage-adapter-spec@npm:2.60.4" @@ -11590,6 +12046,15 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-spec@npm:3.12.0-next.10": + version: 3.12.0-next.10 + resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.10" + dependencies: + "@webex/test-helper-chai": "npm:3.12.0-next.9" + checksum: 10c0/2f94097feaa81158065bad373ba06a659867ed294c39a335ce43bebbcaf2fc077c3e0c8dae3db488d59133472ba2c6ba422d1d738f6c0d4ff4b3d5a79e09f596 + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:3.12.0-next.7": version: 3.12.0-next.7 resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.7" @@ -11674,6 +12139,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-chai@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-chai@npm:3.12.0-next.9" + dependencies: + "@webex/test-helper-file": "npm:3.12.0-next.9" + check-error: "npm:^1.0.2" + lodash: "npm:^4.17.21" + checksum: 10c0/cb221635799a04cc39474892351ddbb417634d1a0b836185a711b651eca8ab1f198aa0ba4b3dbe3c74b225bfff2e17a6e0301ff019c210c32b197738dc3e1863 + languageName: node + linkType: hard + "@webex/test-helper-file@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-file@npm:2.60.4" @@ -11713,6 +12189,19 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-file@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-file@npm:3.12.0-next.9" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/test-helper-make-local-url": "npm:3.12.0-next.9" + es6-promise: "npm:^4.2.8" + file-type: "npm:^16.0.1" + xhr: "npm:^2.5.0" + checksum: 10c0/8a414462b28bc87d00787d4cca2ddb285992811d85692bc324d4329746acd3031b206ae7362f33968490e45431a9278c5feec3b00b5cfd7a72cd6607417e98bb + languageName: node + linkType: hard + "@webex/test-helper-make-local-url@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-make-local-url@npm:2.60.4" @@ -11734,6 +12223,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-make-local-url@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-make-local-url@npm:3.12.0-next.9" + checksum: 10c0/a8034b57e0e3f8be03bf66df176f0893a29a9eb9fb02492f097bb4d0110d421ceaa1d90e10c24bf3947c18f38e5eb5874f8682dc7d21b59fce6e951ea4278313 + languageName: node + linkType: hard + "@webex/test-helper-mocha@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-mocha@npm:2.60.4" @@ -11761,6 +12257,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mocha@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-mocha@npm:3.12.0-next.9" + dependencies: + bowser: "npm:^2.11.0" + checksum: 10c0/0bae7a3b08cbf7f973446d99a9f48f72ac2a47523c4158db04ba4e9f17b0933235fd68d5689662583fd4c676ccb8672e2e4cbbb042cdd3ca1ec82588598b3379 + languageName: node + linkType: hard + "@webex/test-helper-mock-web-socket@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-mock-web-socket@npm:2.60.4" @@ -11782,6 +12287,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-web-socket@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-next.9" + checksum: 10c0/7a3fa1be74a688b325228ebcabd503c4c3ab6501e0dee8d5d8393158e77e8103ffeed5de2f1f4d2a7c3adaafc58715036e30b466329255fa805543faa3c490c2 + languageName: node + linkType: hard + "@webex/test-helper-mock-webex@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-mock-webex@npm:2.60.4" @@ -11815,6 +12327,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-webex@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-mock-webex@npm:3.12.0-next.9" + dependencies: + ampersand-state: "npm:^5.0.3" + es6-promise: "npm:^4.2.8" + lodash: "npm:^4.17.21" + checksum: 10c0/0ea40764fe94ad76056a1b35dcca7c7c6ab02c553762c502289fae46c7519ef9265695d25f66eb65104a1a45ad9cc7436dbfd3726277a04d9500cafd13196a30 + languageName: node + linkType: hard + "@webex/test-helper-refresh-callback@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-refresh-callback@npm:2.60.4" @@ -11836,6 +12359,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-refresh-callback@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-next.9" + checksum: 10c0/3025ca681d1d34a0a22c806eb9451670f3e97c72ad40dae6392d093ec5189ac25e28052179ac1fb02110ba582c325d5c98da0520237bcdfabbd3ae087c49565b + languageName: node + linkType: hard + "@webex/test-helper-retry@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-retry@npm:2.60.4" @@ -11863,6 +12393,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-retry@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-retry@npm:3.12.0-next.9" + dependencies: + es6-promise: "npm:^4.2.8" + checksum: 10c0/ca30b94efd1f7b9db940542d8a9f77f982f19c11cdc91c573d3d4d00ccf33754ae4fee4873c69dfc03d416c12d99da58e7073d103574b831cdfde1b4066c1eec + languageName: node + linkType: hard + "@webex/test-helper-test-users@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-helper-test-users@npm:2.60.4" @@ -11900,6 +12439,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-test-users@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-helper-test-users@npm:3.12.0-next.9" + dependencies: + "@webex/test-helper-retry": "npm:3.12.0-next.9" + "@webex/test-users": "npm:3.12.0-next.9" + lodash: "npm:^4.17.21" + checksum: 10c0/87f6987a503b15e0c92e60af09f7dc0410f377df097204f0dfb7303648ee687558cf561b703ae769cba11c72429319d1a9882cf8af0a80cb554a06243dae1c46 + languageName: node + linkType: hard + "@webex/test-users@npm:2.60.4": version: 2.60.4 resolution: "@webex/test-users@npm:2.60.4" @@ -11942,6 +12492,20 @@ __metadata: languageName: node linkType: hard +"@webex/test-users@npm:3.12.0-next.9": + version: 3.12.0-next.9 + resolution: "@webex/test-users@npm:3.12.0-next.9" + dependencies: + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/test-helper-mocha": "npm:3.12.0-next.9" + btoa: "npm:^1.2.1" + lodash: "npm:^4.17.21" + node-random-name: "npm:^1.0.1" + uuid: "npm:^3.3.2" + checksum: 10c0/3daf55b21cf2dc3c650fd5ca755331522c84b32520c12ccb4db7f94c1696632d60904d298bdd88c1cab69252399504ae26655d3b9f521e5972e232f1c7953ed1 + languageName: node + linkType: hard + "@webex/test-users@npm:^1.157.0": version: 1.161.0 resolution: "@webex/test-users@npm:1.161.0" @@ -12026,6 +12590,25 @@ __metadata: languageName: node linkType: hard +"@webex/web-client-media-engine@npm:3.47.1": + version: 3.47.1 + resolution: "@webex/web-client-media-engine@npm:3.47.1" + dependencies: + "@webex/json-multistream": "npm:^2.4.3" + "@webex/rtcstats": "npm:^1.5.5" + "@webex/ts-events": "npm:^1.2.1" + "@webex/ts-sdp": "npm:1.8.2" + "@webex/web-capabilities": "npm:^1.13.1" + "@webex/web-media-effects": "npm:2.37.0" + "@webex/webrtc-core": "npm:2.15.0" + async: "npm:^3.2.4" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10c0/4b5997222dc2f83de408f010f33087fde99576b67be415f18338f4287c3de74b74ef00485f503e88e293b1158a21a9a464a2c306ffc31aee2ac751baeac36849 + languageName: node + linkType: hard + "@webex/web-media-effects@npm:2.37.0": version: 2.37.0 resolution: "@webex/web-media-effects@npm:2.37.0" @@ -12104,6 +12687,106 @@ __metadata: languageName: node linkType: hard +"@webex/webex-core@npm:3.12.0-next.54": + version: 3.12.0-next.54 + resolution: "@webex/webex-core@npm:3.12.0-next.54" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/5a372bdb4be5c73e7f1ab835abc0cacdb9203b10b0e748220cd2c2ffa83902dd604f8507441192e420cb392b4f512492c57af649d3d59267b6992ce9dcabf580 + languageName: node + linkType: hard + +"@webex/webex-core@npm:3.12.0-next.55": + version: 3.12.0-next.55 + resolution: "@webex/webex-core@npm:3.12.0-next.55" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/ba0129b3aabc25632cad8971fd6bbd3cb9e9d3c982e479e9b7dffd04d7bf974c4d4061b628141056ec92177c69c0aad0f522cae3483f051e24a96dc6d4bdbcac + languageName: node + linkType: hard + +"@webex/webex-core@npm:3.12.0-next.56": + version: 3.12.0-next.56 + resolution: "@webex/webex-core@npm:3.12.0-next.56" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/79e68aa7965d6e1f3501a23252d48f6e9eb32739dd2b939df3e0d2eedd99554370f79443624c6cc9e695cb31e791f376af17357d50aa4e86271706aaeab048e6 + languageName: node + linkType: hard + +"@webex/webex-core@npm:3.12.0-next.57": + version: 3.12.0-next.57 + resolution: "@webex/webex-core@npm:3.12.0-next.57" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/63473635321a4d1e3f505224f70eea4757de1dc2ee3bef499e81570453e34253b62152d6076b72511437fc99bb6d398252f581f29d92a66820270e35277fe9d9 + languageName: node + linkType: hard + +"@webex/webex-core@npm:3.12.0-next.60": + version: 3.12.0-next.60 + resolution: "@webex/webex-core@npm:3.12.0-next.60" + dependencies: + "@webex/common": "npm:3.12.0-next.9" + "@webex/common-timers": "npm:3.12.0-next.10" + "@webex/http-core": "npm:3.12.0-next.9" + "@webex/storage-adapter-spec": "npm:3.12.0-next.10" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/1d760242c81296c2150c40616e34e4c985a6fa3345a688c1e9cf58bd82fdc661914659e118ba07011382600db5e4d3cf3ab02f5fd91af4251d1a748bb5ab1c78 + languageName: node + linkType: hard + "@webex/webrtc-core@npm:2.14.4": version: 2.14.4 resolution: "@webex/webrtc-core@npm:2.14.4" @@ -12119,6 +12802,21 @@ __metadata: languageName: node linkType: hard +"@webex/webrtc-core@npm:2.15.0": + version: 2.15.0 + resolution: "@webex/webrtc-core@npm:2.15.0" + dependencies: + "@webex/ts-events": "npm:^1.2.1" + "@webex/web-capabilities": "npm:^1.13.1" + "@webex/web-media-effects": "npm:2.37.0" + events: "npm:^3.3.0" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + webrtc-adapter: "npm:^8.1.2" + checksum: 10c0/13e741912dec8be80b538bcfe47771423743acb2994e088f6c4edf91c69aa6ae0f8df8ec4cf539138ad7f392803a82b2be8828e09907fd237dcc388b655c0807 + languageName: node + linkType: hard + "@webex/widgets@workspace:*, @webex/widgets@workspace:packages/@webex/widgets": version: 0.0.0-use.local resolution: "@webex/widgets@workspace:packages/@webex/widgets"