diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b25871a71..99d5eb030 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,15 +34,15 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version-file: ".nvmrc" - registry-url: "https://registry.npmjs.org" - cache: "yarn" + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + cache: 'yarn' - name: Install Dependencies uses: actions/cache@v4 id: cache-dependencies with: - path: "**/node_modules" + path: '**/node_modules' key: node-modules-${{ hashFiles('./yarn.lock') }} - name: Install if cache miss @@ -88,14 +88,14 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version-file: ".nvmrc" - registry-url: "https://registry.npmjs.org" - cache: "yarn" + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + cache: 'yarn' - name: Restore Dependencies Cache uses: actions/cache/restore@v4 with: - path: "**/node_modules" + path: '**/node_modules' key: node-modules-${{ hashFiles('./yarn.lock') }} - name: Check for Changed Packages @@ -138,7 +138,7 @@ jobs: if: steps.check-changes.outputs.has_changes == 'true' uses: actions/cache/save@v4 with: - path: "**/dist" + path: '**/dist' key: dist-${{ env.rid }} - name: Deploy Packages @@ -149,7 +149,7 @@ jobs: publish-documentation: name: Publish - Documentation - needs: [publish-npm,analyze-changes] + needs: [publish-npm, analyze-changes] runs-on: ubuntu-latest steps: @@ -229,20 +229,20 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version-file: ".nvmrc" - registry-url: "https://registry.npmjs.org" - cache: "yarn" + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + cache: 'yarn' - name: Restore Dependencies Cache uses: actions/cache/restore@v4 with: - path: "**/node_modules" + path: '**/node_modules' key: node-modules-${{ hashFiles('./yarn.lock') }} - name: Restore Distributables Cache uses: actions/cache/restore@v4 with: - path: "**/dist" + path: '**/dist' key: dist-${{ env.rid }} - name: Synchronize Packages diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f81801455..e90e6261e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -3,6 +3,8 @@ run-name: ${{ github.actor }} is running Pull Request CI on: pull_request_target: + branches: + - next types: [opened, labeled, reopened, synchronize] workflow_dispatch: diff --git a/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.tsx b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.tsx new file mode 100644 index 000000000..0d9ea5034 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.tsx @@ -0,0 +1,95 @@ +import React, {useMemo} from 'react'; +import {Avatar} from '@momentum-design/components/dist/react'; +import {withMetrics} from '@webex/cc-ui-logging'; +import {RealtimeTranscriptComponentProps} from '../task.types'; +import './real-time-transcripts.style.scss'; + +const formatSpeaker = (speaker?: string) => speaker || 'Unknown'; + +const RealTimeTranscriptComponent: React.FC = ({ + ivrTranscript = '', + liveTranscriptEntries = [], + activeTab = 'live', + onTabChange, + className, +}) => { + const sortedEntries = useMemo( + () => + [...liveTranscriptEntries].sort((a, b) => { + if (a.timestamp === b.timestamp) return 0; + return a.timestamp > b.timestamp ? 1 : -1; + }), + [liveTranscriptEntries] + ); + + return ( +
+
+ +
+ + {activeTab === 'ivr' ? ( +
+ {ivrTranscript || 'No IVR transcript available.'} +
+ ) : ( +
+ {sortedEntries.length === 0 ? ( +
No live transcript available.
+ ) : ( + <> + {sortedEntries[0].event ? ( +
+ {sortedEntries[0].event} +
+ ) : null} + {sortedEntries.map((entry) => ( +
+
+ {entry.avatarUrl ? ( + {formatSpeaker(entry.speaker)} + ) : ( + + {entry.initials || (entry.isCustomer ? 'CU' : 'YO')} + + )} +
+
+
+ {formatSpeaker(entry.speaker)} + {entry.displayTime ? ( + {entry.displayTime} + ) : null} +
+

{entry.message}

+
+
+ ))} + + )} +
+ )} +
+ ); +}; + +const RealTimeTranscriptComponentWithMetrics = withMetrics(RealTimeTranscriptComponent, 'RealTimeTranscript'); + +export default RealTimeTranscriptComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcripts.style.scss b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcripts.style.scss new file mode 100644 index 000000000..d22555b0e --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcripts.style.scss @@ -0,0 +1,103 @@ +.real-time-transcript { + background: var(--mds-color-theme-background-primary-normal); + border-radius: 0.5rem; + display: flex; + flex-direction: column; + min-height: 12rem; + padding: 0.75rem 0.875rem; +} + +.real-time-transcript__tabs { + align-items: center; + column-gap: 1.5rem; + display: flex; + margin-bottom: 0.75rem; +} + +.real-time-transcript__tab { + background: transparent; + border: 0; + border-bottom: 0.125rem solid transparent; + color: var(--mds-color-theme-text-secondary-normal); + cursor: pointer; + font-size: 0.9375rem; + font-weight: 500; + line-height: 1.25rem; + padding: 0.125rem 0; +} + +.real-time-transcript__tab--active { + border-bottom-color: var(--mds-color-theme-text-primary-normal); + color: var(--mds-color-theme-text-primary-normal); + font-weight: 700; +} + +.real-time-transcript__content { + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; + row-gap: 0.875rem; +} + +.real-time-transcript__event { + color: var(--mds-color-theme-text-secondary-normal); + font-size: 0.75rem; + line-height: 1rem; + text-align: center; +} + +.real-time-transcript__item { + align-items: flex-start; + column-gap: 0.625rem; + display: flex; +} + +.real-time-transcript__avatar-wrap { + flex-shrink: 0; + height: 1.75rem; + width: 1.75rem; +} + +.real-time-transcript__avatar-image { + border-radius: 50%; + display: block; + height: 100%; + object-fit: cover; + width: 100%; +} + +.real-time-transcript__avatar-fallback { + --mdc-avatar-size: 1.75rem; +} + +.real-time-transcript__text-block { + min-width: 0; +} + +.real-time-transcript__meta { + color: var(--mds-color-theme-text-secondary-normal); + display: flex; + font-size: 0.75rem; + line-height: 1rem; +} + +.real-time-transcript__time { + color: #2e6de5; + margin-left: 0.5rem; + text-decoration: underline; +} + +.real-time-transcript__message { + color: var(--mds-color-theme-text-primary-normal); + font-size: 1.0625rem; + line-height: 1.5rem; + margin: 0.125rem 0 0; +} + +.real-time-transcript__empty { + color: var(--mds-color-theme-text-secondary-normal); + font-size: 0.875rem; + line-height: 1.25rem; + padding: 1rem 0.125rem; +} diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index aac63d7ca..a1007d881 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -153,6 +153,28 @@ export type TaskListComponentProps = Pick< > & Partial>; +export type TranscriptTab = 'ivr' | 'live'; + +export interface RealtimeTranscriptEntry { + id: string; + speaker: string; + message: string; + timestamp: number; + displayTime?: string; + event?: string; + isCustomer?: boolean; + avatarUrl?: string; + initials?: string; +} + +export interface RealtimeTranscriptComponentProps { + ivrTranscript?: string; + liveTranscriptEntries?: RealtimeTranscriptEntry[]; + activeTab?: TranscriptTab; + onTabChange?: (tab: TranscriptTab) => void; + className?: string; +} + /** * Interface representing the properties for control actions on a task. */ diff --git a/packages/contact-center/cc-components/src/index.ts b/packages/contact-center/cc-components/src/index.ts index d4d692fdb..900cbb43e 100644 --- a/packages/contact-center/cc-components/src/index.ts +++ b/packages/contact-center/cc-components/src/index.ts @@ -5,6 +5,7 @@ import CallControlCADComponent from './components/task/CallControlCAD/call-contr import IncomingTaskComponent from './components/task/IncomingTask/incoming-task'; import TaskListComponent from './components/task/TaskList/task-list'; import OutdialCallComponent from './components/task/OutdialCall/outdial-call'; +import RealtimeTranscriptComponent from './components/task/RealTimeTranscript/real-time-transcript'; export { UserStateComponent, @@ -14,6 +15,7 @@ export { IncomingTaskComponent, TaskListComponent, OutdialCallComponent, + RealtimeTranscriptComponent, }; export * from './components/StationLogin/constants'; export * from './components/StationLogin/station-login.types'; diff --git a/packages/contact-center/cc-components/src/wc.ts b/packages/contact-center/cc-components/src/wc.ts index 1553aab77..074660b44 100644 --- a/packages/contact-center/cc-components/src/wc.ts +++ b/packages/contact-center/cc-components/src/wc.ts @@ -6,6 +6,7 @@ import CallControlCADComponent from './components/task/CallControl/call-control' import IncomingTaskComponent from './components/task/IncomingTask/incoming-task'; import TaskListComponent from './components/task/TaskList/task-list'; import OutdialCallComponent from './components/task/OutdialCall/outdial-call'; +import RealtimeTranscriptComponent from './components/task/RealTimeTranscript/real-time-transcript'; const WebUserState = r2wc(UserStateComponent, { props: { @@ -106,3 +107,16 @@ const WebOutdialCallComponent = r2wc(OutdialCallComponent); if (!customElements.get('component-cc-out-dial-call')) { customElements.define('component-cc-out-dial-call', WebOutdialCallComponent); } + +const WebRealtimeTranscriptComponent = r2wc(RealtimeTranscriptComponent, { + props: { + ivrTranscript: 'string', + liveTranscriptEntries: 'json', + activeTab: 'string', + onTabChange: 'function', + className: 'string', + }, +}); +if (!customElements.get('component-cc-realtime-transcript')) { + customElements.define('component-cc-realtime-transcript', WebRealtimeTranscriptComponent); +} diff --git a/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.tsx b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.tsx new file mode 100644 index 000000000..8619b0926 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {fireEvent, render, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import RealTimeTranscriptComponent from '../../../../src/components/task/RealTimeTranscript/real-time-transcript'; +import {RealtimeTranscriptComponentProps} from '../../../../src/components/task/task.types'; + +describe('RealTimeTranscriptComponent', () => { + const defaultProps: RealtimeTranscriptComponentProps = { + ivrTranscript: 'IVR summary text', + activeTab: 'live', + liveTranscriptEntries: [ + { + id: '2', + speaker: '%Customer%', + message: 'Customer message', + timestamp: 2, + displayTime: '00:02', + isCustomer: true, + }, + { + id: '1', + speaker: '%You%', + message: 'Agent message', + timestamp: 1, + displayTime: '00:01', + }, + ], + }; + + it('renders live transcript entries and sorts by timestamp', () => { + render(); + + const messages = screen.getAllByTestId('real-time-transcript:item'); + expect(messages).toHaveLength(2); + expect(messages[0]).toHaveTextContent('Agent message'); + expect(messages[1]).toHaveTextContent('Customer message'); + }); + + it('renders ivr content when ivr tab active', () => { + render(); + expect(screen.getByTestId('real-time-transcript:ivr-content')).toHaveTextContent('IVR summary text'); + }); + + it('calls onTabChange when live tab clicked', () => { + const onTabChange = jest.fn(); + render(); + + fireEvent.click(screen.getByTestId('real-time-transcript:live-tab')); + expect(onTabChange).toHaveBeenCalledWith('live'); + }); +}); diff --git a/packages/contact-center/cc-widgets/src/index.ts b/packages/contact-center/cc-widgets/src/index.ts index ea1562e11..7fa69a1df 100644 --- a/packages/contact-center/cc-widgets/src/index.ts +++ b/packages/contact-center/cc-widgets/src/index.ts @@ -1,6 +1,6 @@ import {StationLogin} from '@webex/cc-station-login'; import {UserState} from '@webex/cc-user-state'; -import {IncomingTask, TaskList, CallControl, CallControlCAD, OutdialCall} from '@webex/cc-task'; +import {IncomingTask, TaskList, CallControl, CallControlCAD, OutdialCall, RealtimeTranscript} from '@webex/cc-task'; import {DigitalChannels} from '@webex/cc-digital-channels'; import store from '@webex/cc-store'; import '@momentum-ui/core/css/momentum-ui.min.css'; @@ -13,6 +13,7 @@ export { CallControlCAD, TaskList, OutdialCall, + RealtimeTranscript, DigitalChannels, store, }; diff --git a/packages/contact-center/cc-widgets/src/wc.ts b/packages/contact-center/cc-widgets/src/wc.ts index 5fd608367..bd3cb3145 100644 --- a/packages/contact-center/cc-widgets/src/wc.ts +++ b/packages/contact-center/cc-widgets/src/wc.ts @@ -2,7 +2,7 @@ import r2wc from '@r2wc/react-to-web-component'; import {StationLogin} from '@webex/cc-station-login'; import {UserState} from '@webex/cc-user-state'; import store from '@webex/cc-store'; -import {TaskList, IncomingTask, CallControl, CallControlCAD, OutdialCall} from '@webex/cc-task'; +import {TaskList, IncomingTask, CallControl, CallControlCAD, OutdialCall, RealtimeTranscript} from '@webex/cc-task'; import {DigitalChannels} from '@webex/cc-digital-channels'; const WebUserState = r2wc(UserState, { @@ -53,6 +53,15 @@ const WebCallControlCAD = r2wc(CallControlCAD, { }); const WebOutdialCall = r2wc(OutdialCall, {}); +const WebRealtimeTranscript = r2wc(RealtimeTranscript, { + props: { + ivrTranscript: 'string', + liveTranscriptEntries: 'json', + activeTab: 'string', + onTabChange: 'function', + className: 'string', + }, +}); const WebDigitalChannels = r2wc(DigitalChannels, {}); @@ -66,6 +75,7 @@ const components = [ {name: 'widget-cc-call-control', component: WebCallControl}, {name: 'widget-cc-outdial-call', component: WebOutdialCall}, {name: 'widget-cc-call-control-cad', component: WebCallControlCAD}, + {name: 'widget-cc-realtime-transcript', component: WebRealtimeTranscript}, {name: 'widget-cc-digital-channels', component: WebDigitalChannels}, ]; diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index aacf58607..fd63a8089 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.11.0-next.20", + "@webex/contact-center": "3.12.0-next.1", "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 5ade1368c..5db96c488 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -13,6 +13,8 @@ import { AgentLoginProfile, LoginOptions, WithWebex, + RealtimeTranscriptionData, + RealtimeTranscriptLine, } from './store.types'; import {getFeatureFlags} from './util'; @@ -51,6 +53,8 @@ class Store implements IStore { isMuted: boolean = false; isDigitalChannelsInitialized: boolean = false; dataCenter: string = ''; + realtimeTranscriptionData: RealtimeTranscriptionData = {}; + realtimeTranscriptLines: RealtimeTranscriptLine[] = []; constructor() { makeAutoObservable(this, { diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 0d3d6a2ae..bb77ea15d 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -95,6 +95,46 @@ type IdleCode = { isDefault: boolean; }; +type RealtimeTranscriptionData = { + content?: string; + conversationId?: string; + isFinal?: boolean; + languageCode?: string; + messageId?: string; + orgId?: string; + publishTimestamp?: number | string; + role?: string; + trackingId?: string; + utteranceId?: string; +}; + +type RealtimeTranscriptionEventPayload = { + data?: { + agentId?: string; + content?: string; + publishTimestamp?: number | string; + role?: string; + data?: RealtimeTranscriptionData; + notifDetails?: { + actionEvent?: string; + }; + notifType?: string; + orgId?: string; + }; + orgId?: string; + trackingId?: string; + type?: string; +}; + +type RealtimeTranscriptLine = { + messageId: string; + role: string; + caller: string; + content: string; + publishTimestamp: number; + conversationId?: string; +}; + interface IStore { featureFlags: {[key: string]: boolean}; teams: Team[]; @@ -128,6 +168,8 @@ interface IStore { isAddressBookEnabled: boolean; isDigitalChannelsInitialized: boolean; dataCenter: string; + realtimeTranscriptionData: RealtimeTranscriptionData; + realtimeTranscriptLines: RealtimeTranscriptLine[]; init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise; registerCC(webex?: WithWebex['webex']): Promise; } @@ -207,6 +249,7 @@ enum TASK_EVENTS { TASK_MERGED = 'task:merged', TASK_POST_CALL_ACTIVITY = 'task:postCallActivity', TASK_OUTDIAL_FAILED = 'task:outdialFailed', + REAL_TIME_TRANSCRIPTION = 'REAL_TIME_TRANSCRIPTION', } // TODO: remove this once cc sdk exports this enum // Events that are received on the contact center SDK @@ -319,6 +362,9 @@ export type { PaginatedListParams, FetchPaginatedList, TransformPaginatedData, + RealtimeTranscriptionData, + RealtimeTranscriptionEventPayload, + RealtimeTranscriptLine, }; export { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 87aeb93f4..0fd240114 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -22,6 +22,7 @@ import { Profile, AgentLoginProfile, ERROR_TRIGGERING_IDLE_CODES, + RealtimeTranscriptionEventPayload, } from './store.types'; import Store from './store'; import { @@ -41,6 +42,10 @@ class StoreWrapper implements IStoreWrapper { onTaskAssigned?: (task: ITask) => void; onTaskSelected?: (task: ITask, isClicked: boolean) => void; onErrorCallback?: (widgetName: string, error: Error) => void; + private realtimeTranscriptionListeners: Record< + string, + (payload: NonNullable) => void + > = {}; constructor() { this.store = Store.getInstance(); @@ -144,6 +149,14 @@ class StoreWrapper implements IStoreWrapper { return this.store.dataCenter; } + get realtimeTranscriptionData() { + return this.store.realtimeTranscriptionData; + } + + get realtimeTranscriptLines() { + return this.store.realtimeTranscriptLines; + } + setDataCenter = (value: string): void => { this.store.dataCenter = value; }; @@ -231,7 +244,7 @@ class StoreWrapper implements IStoreWrapper { if (isIncomingTask(task, this.agentId)) return; runInAction(() => { - // Determine if the new task is the same as the current task + // Determine if the new task is the same as the current task. let isSameTask = false; if (task && this.currentTask) { isSameTask = task.data.interactionId === this.currentTask.data.interactionId; @@ -419,6 +432,11 @@ class StoreWrapper implements IStoreWrapper { handleTaskRemove = (taskToRemove: ITask) => { if (taskToRemove) { + const taskId = taskToRemove.data?.interactionId; + if (taskId && this.realtimeTranscriptionListeners[taskId]) { + taskToRemove.off(TASK_EVENTS.REAL_TIME_TRANSCRIPTION, this.realtimeTranscriptionListeners[taskId]); + delete this.realtimeTranscriptionListeners[taskId]; + } taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); taskToRemove.off(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(taskToRemove, reason)); @@ -452,6 +470,13 @@ class StoreWrapper implements IStoreWrapper { } runInAction(() => { + if (taskToRemove) { + const removedTaskId = taskToRemove.data?.interactionId; + if (removedTaskId && this.store.currentTask?.data?.interactionId === removedTaskId) { + this.store.realtimeTranscriptionData = {}; + this.store.realtimeTranscriptLines = []; + } + } if (taskToRemove && this.store.currentTask?.data.interactionId === taskToRemove.data.interactionId) { this.setCurrentTask(null); } @@ -599,6 +624,14 @@ class StoreWrapper implements IStoreWrapper { task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.refreshTaskList); task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, this.refreshTaskList); + const taskId = task.data?.interactionId; + if (taskId && !this.realtimeTranscriptionListeners[taskId]) { + this.realtimeTranscriptionListeners[taskId] = (payload: NonNullable) => + this.handleRealtimeTranscription(payload); + } + if (taskId && this.realtimeTranscriptionListeners[taskId]) { + task.on(TASK_EVENTS.REAL_TIME_TRANSCRIPTION, this.realtimeTranscriptionListeners[taskId]); + } // Register media event listener for browser devices if (this.deviceType === DEVICE_TYPE_BROWSER) { @@ -705,6 +738,86 @@ class StoreWrapper implements IStoreWrapper { } }; + private findTranscriptPosition = (messageId: string, timestamp: number) => { + let existingIndex = -1; + let insertPosition = -1; + + for (let i = this.store.realtimeTranscriptLines.length - 1; i >= 0; i -= 1) { + const line = this.store.realtimeTranscriptLines[i]; + if (line.messageId === messageId) { + existingIndex = i; + break; + } + if (insertPosition === -1 && line.publishTimestamp <= timestamp) { + insertPosition = i; + } + } + + return {existingIndex, insertPosition}; + }; + + handleRealtimeTranscription = (payload: NonNullable) => { + // SDK emits task events with the unwrapped inner data: task.emit(eventType, websocketPayload.data) + // So `payload` here is RealtimeTranscriptionEventPayload['data'], and `payload.data` is RealtimeTranscriptionData + const data = payload?.data; + if (!data?.messageId) return; + + const role = (payload?.role || data.role || 'CALLER').toUpperCase(); + const caller = role === 'CALLER' || role === 'CUSTOMER' ? 'Customer' : 'Agent'; + const publishTimestampRaw = payload?.publishTimestamp || data.publishTimestamp; + const publishTimestamp = + typeof publishTimestampRaw === 'number' + ? publishTimestampRaw + : Number.parseInt(`${publishTimestampRaw || Date.now()}`, 10); + + const normalizedRealtimeData = { + ...data, + role, + caller, + content: (payload?.content || data.content || '').trim(), + publishTimestamp: Number.isNaN(publishTimestamp) ? Date.now() : publishTimestamp, + }; + if (!normalizedRealtimeData.content) return; + + runInAction(() => { + this.store.realtimeTranscriptionData = normalizedRealtimeData; + + const {existingIndex, insertPosition} = this.findTranscriptPosition( + data.messageId, + normalizedRealtimeData.publishTimestamp + ); + + if (existingIndex >= 0) { + const currentLine = this.store.realtimeTranscriptLines[existingIndex]; + if ((currentLine.content || '') === (normalizedRealtimeData.content || '')) { + return; + } + const updatedLines = [...this.store.realtimeTranscriptLines]; + updatedLines[existingIndex] = { + ...currentLine, + caller, + content: normalizedRealtimeData.content || '', + publishTimestamp: normalizedRealtimeData.publishTimestamp, + conversationId: normalizedRealtimeData.conversationId, + }; + this.store.realtimeTranscriptLines = updatedLines; + return; + } + + const newLine = { + messageId: data.messageId, + role, + caller, + content: normalizedRealtimeData.content || '', + publishTimestamp: normalizedRealtimeData.publishTimestamp, + conversationId: normalizedRealtimeData.conversationId, + }; + const updatedLines = [...this.store.realtimeTranscriptLines]; + updatedLines.splice(insertPosition + 1, 0, newLine); + this.store.realtimeTranscriptLines = updatedLines; + }); + }; + getBuddyAgents = async ( mediaType: string = this.currentTask.data.interaction.mediaType ): Promise> => { @@ -801,6 +914,9 @@ class StoreWrapper implements IStoreWrapper { this.setConsultStartTimeStamp(undefined); this.setTeamId(''); this.setDigitalChannelsInitialized(false); + this.store.realtimeTranscriptionData = {}; + this.store.realtimeTranscriptLines = []; + this.realtimeTranscriptionListeners = {}; }); }; diff --git a/packages/contact-center/task/src/ReatimeTranscript/index.tsx b/packages/contact-center/task/src/ReatimeTranscript/index.tsx new file mode 100644 index 000000000..53a766719 --- /dev/null +++ b/packages/contact-center/task/src/ReatimeTranscript/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; + +import store from '@webex/cc-store'; +import {RealtimeTranscriptComponent} from '@webex/cc-components'; +import {useRealtimeTranscript} from '../helper'; +import {RealtimeTranscriptProps} from '../task.types'; + +const RealtimeTranscriptInternal: React.FunctionComponent = observer((props) => { + const {currentTask, realtimeTranscriptLines} = store; + const result = useRealtimeTranscript({ + ...props, + currentTaskId: currentTask?.data?.interactionId, + realtimeTranscriptLines, + }); + return ; +}); + +const RealtimeTranscript: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('RealtimeTranscript', error); + }} + > + + + ); +}; + +export {RealtimeTranscript}; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index c3e2b6129..81db64568 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -4,6 +4,8 @@ import { useCallControlProps, UseTaskListProps, UseTaskProps, + UseRealtimeTranscriptInternalProps, + RealtimeTranscriptEntry, useOutdialCallProps, TargetType, TARGET_TYPE, @@ -17,6 +19,7 @@ import store, { Participant, findMediaResourceId, MEDIA_TYPE_TELEPHONY_LOWER, + RealtimeTranscriptionData, } from '@webex/cc-store'; import {getControlsVisibility} from './Utils/task-util'; import {TIMER_LABEL_CONSULTING} from './Utils/constants'; @@ -27,6 +30,28 @@ import {OutdialAniEntriesResponse} from '@webex/contact-center/dist/types/servic const ENGAGED_LABEL = 'ENGAGED'; const ENGAGED_USERNAME = 'Engaged'; +const getTranscriptSpeaker = (payload: RealtimeTranscriptionData): string => { + const role = payload.role?.toUpperCase(); + if (role === 'AGENT') return 'Agent'; + if (role === 'CUSTOMER') return 'Customer'; + + return role; +}; + +const getTranscriptTimestamp = (payload: RealtimeTranscriptionData): number => { + const rawTimestamp = payload.publishTimestamp; + const parsedTimestamp = + typeof rawTimestamp === 'number' ? rawTimestamp : Number.parseInt(`${rawTimestamp || ''}`, 10); + return Number.isNaN(parsedTimestamp) ? Date.now() : parsedTimestamp; +}; + +const isCustomerSpeaker = (payload: RealtimeTranscriptionData, speaker: string): boolean => { + const role = (payload.role || '').toUpperCase(); + if (role) return role === 'CUSTOMER' || role === 'CALLER'; + const normalizedSpeaker = speaker.toLowerCase(); + return normalizedSpeaker.includes('customer') || normalizedSpeaker.includes('caller'); +}; + // Hook for managing the task list export const useTaskList = (props: UseTaskListProps) => { const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; @@ -146,6 +171,57 @@ export const useTaskList = (props: UseTaskListProps) => { return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; }; +export const useRealtimeTranscript = (props: UseRealtimeTranscriptInternalProps) => { + const { + ivrTranscript = '', + liveTranscriptEntries = [], + activeTab = 'live', + onTabChange, + className, + currentTaskId, + realtimeTranscriptLines = [], + } = props; + const mappedRealtimeEntries = useMemo(() => { + if (!currentTaskId) return liveTranscriptEntries; + + const transcriptLines = realtimeTranscriptLines; + if (!transcriptLines.length) { + return liveTranscriptEntries; + } + + return transcriptLines.map((line, index) => { + const payload: RealtimeTranscriptionData = { + messageId: line.messageId, + conversationId: line.conversationId, + role: line.role, + content: line.content, + publishTimestamp: line.publishTimestamp, + }; + + const timestamp = getTranscriptTimestamp(payload); + const speaker = getTranscriptSpeaker(payload); + + return { + id: `${currentTaskId}-${line.messageId}-${index}`, + speaker, + message: line.content, + timestamp, + displayTime: new Date(timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}), + event: 'REAL_TIME_TRANSCRIPTION', + isCustomer: isCustomerSpeaker(payload, speaker), + }; + }); + }, [currentTaskId, realtimeTranscriptLines, liveTranscriptEntries]); + + return { + ivrTranscript, + liveTranscriptEntries: mappedRealtimeEntries, + activeTab, + onTabChange, + className, + }; +}; + export const useIncomingTask = (props: UseTaskProps) => { const {onAccepted, onRejected, deviceType, incomingTask, logger} = props; const isBrowser = deviceType === 'BROWSER'; @@ -1061,7 +1137,7 @@ export const useOutdialCall = (props: useOutdialCallProps) => { // Only pass origin if it's defined and not empty const outdialArgs = origin ? [destination, origin] : [destination]; - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + // @ts-expect-error To be fixed in SDK typings - CAI-6762 cc.startOutdial(...outdialArgs) .then((response) => { logger.info('Outdial call started', response); diff --git a/packages/contact-center/task/src/index.ts b/packages/contact-center/task/src/index.ts index a8e8eedc6..b18d307ee 100644 --- a/packages/contact-center/task/src/index.ts +++ b/packages/contact-center/task/src/index.ts @@ -3,4 +3,5 @@ import {TaskList} from './TaskList'; import {CallControl} from './CallControl'; import {OutdialCall} from './OutdialCall'; import {CallControlCAD} from './CallControlCAD'; -export {IncomingTask, TaskList, CallControl, OutdialCall, CallControlCAD}; +import {RealtimeTranscript} from './ReatimeTranscript'; +export {IncomingTask, TaskList, CallControl, OutdialCall, CallControlCAD, RealtimeTranscript}; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index c0c759382..0660cd97e 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -1,4 +1,12 @@ -import {TaskProps, ControlProps, OutdialCallProps} from '@webex/cc-components'; +import { + TaskProps, + ControlProps, + OutdialCallProps, + RealtimeTranscriptComponentProps, + RealtimeTranscriptEntry, + TranscriptTab, +} from '@webex/cc-components'; +import {RealtimeTranscriptLine} from '@webex/cc-store'; export type UseTaskProps = Pick & Partial>; @@ -10,6 +18,18 @@ export type IncomingTaskProps = Pick & Partial>; +export type RealtimeTranscriptProps = Pick< + RealtimeTranscriptComponentProps, + 'ivrTranscript' | 'liveTranscriptEntries' +> & + Partial>; + +export type UseRealtimeTranscriptProps = RealtimeTranscriptProps; +export type UseRealtimeTranscriptInternalProps = UseRealtimeTranscriptProps & { + currentTaskId?: string; + realtimeTranscriptLines?: RealtimeTranscriptLine[]; +}; + export type CallControlProps = Partial< Pick< ControlProps, @@ -32,6 +52,8 @@ export type useCallControlProps = Pick< Partial>; export type useOutdialCallProps = Pick; + +export type {RealtimeTranscriptEntry, TranscriptTab}; export interface OutdialProps { /** * Flag to determine if the address book is enabled. diff --git a/packages/contact-center/task/tests/RealtimeTranscript/index.tsx b/packages/contact-center/task/tests/RealtimeTranscript/index.tsx new file mode 100644 index 000000000..8a53228a6 --- /dev/null +++ b/packages/contact-center/task/tests/RealtimeTranscript/index.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import {RealtimeTranscript} from '../../src/index'; +import * as helper from '../../src/helper'; +import store from '@webex/cc-store'; + +jest.mock('@webex/cc-store', () => ({ + currentTask: { + data: { + interactionId: 'test-interaction-id', + }, + }, + realtimeTranscriptLines: [], + logger: { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }, +})); + +describe('RealtimeTranscript Widget', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('passes props to useRealtimeTranscript hook', () => { + const spy = jest.spyOn(helper, 'useRealtimeTranscript'); + const transcriptProps = { + ivrTranscript: 'ivr', + liveTranscriptEntries: [{id: '1', speaker: 'Agent', message: 'Hello', timestamp: 1}], + }; + + render(); + + expect(spy).toHaveBeenCalledWith({ + ...transcriptProps, + currentTaskId: 'test-interaction-id', + realtimeTranscriptLines: [], + }); + expect(screen.getByTestId('real-time-transcript:root')).toBeInTheDocument(); + }); + + it('renders fallback when an error is thrown', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + jest.spyOn(helper, 'useRealtimeTranscript').mockImplementation(() => { + throw new Error('RealtimeTranscript test error'); + }); + + const {container} = render(); + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('RealtimeTranscript', expect.any(Error)); + }); +}); diff --git a/packages/contact-center/ui-logging/src/metricsLogger.ts b/packages/contact-center/ui-logging/src/metricsLogger.ts index 02f2b8d87..c3d7429f5 100644 --- a/packages/contact-center/ui-logging/src/metricsLogger.ts +++ b/packages/contact-center/ui-logging/src/metricsLogger.ts @@ -87,16 +87,10 @@ export function havePropsChanged(prev: any, next: any): boolean { if (prevKeys.length !== nextKeys.length) return true; - // Check if any primitive values changed + // Shallow comparison: detect any value change (primitives by value, objects/arrays by reference) for (const key of prevKeys) { - const prevVal = prev[key]; - const nextVal = next[key]; - - if (prevVal === nextVal) continue; - if (typeof prevVal !== 'object' || prevVal === null) return true; - if (typeof nextVal !== 'object' || nextVal === null) return true; + if (prev[key] !== next[key]) return true; } - // All shallow comparisons passed, consider props unchanged return false; } diff --git a/packages/contact-center/ui-logging/tests/metricsLogger.test.ts b/packages/contact-center/ui-logging/tests/metricsLogger.test.ts index f7ecf15e5..2d89d9af0 100644 --- a/packages/contact-center/ui-logging/tests/metricsLogger.test.ts +++ b/packages/contact-center/ui-logging/tests/metricsLogger.test.ts @@ -71,9 +71,16 @@ describe('metricsLogger', () => { expect(havePropsChanged(obj1, obj2)).toBe(true); }); - it('should return false when nested values differ', () => { + it('should return true when object references differ', () => { const obj1 = {a: {b: 1}}; const obj2 = {a: {b: 2}}; + expect(havePropsChanged(obj1, obj2)).toBe(true); + }); + + it('should return false when object references are the same', () => { + const shared = {b: 1}; + const obj1 = {a: shared}; + const obj2 = {a: shared}; expect(havePropsChanged(obj1, obj2)).toBe(false); }); diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 7583180a3..3a931485d 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -8,6 +8,7 @@ import { CallControlCAD, store, OutdialCall, + RealtimeTranscript, } from '@webex/cc-widgets'; import {StationLogoutResponse} from '@webex/contact-center'; import {ERROR_TRIGGERING_IDLE_CODES} from '@webex/cc-store'; @@ -39,6 +40,7 @@ const defaultWidgets = { callControl: true, callControlCAD: true, outdialCall: true, + realtimeTranscript: true, }; function App() { @@ -954,6 +956,16 @@ function App() { )} + {selectedWidgets.realtimeTranscript && store.currentTask && ( +
+
+
+ Realtime Transcript + +
+
+
+ )} {selectedWidgets.outdialCall && (
diff --git a/yarn.lock b/yarn.lock index f3ca3f2b3..e7f37a429 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@aml-org/amf-antlr-parsers@npm:0.8.34": - version: 0.8.34 - resolution: "@aml-org/amf-antlr-parsers@npm:0.8.34" - checksum: 10c0/0a8fa2f13df8dd027364e27a258ae23fe6592ea8c55cd898424132b663d3b39ab98d1f1e44f63d808b1c5b47f1e9ec55752ac495b9a56159944c506579eef778 +"@aml-org/amf-antlr-parsers@npm:0.8.28": + version: 0.8.28 + resolution: "@aml-org/amf-antlr-parsers@npm:0.8.28" + checksum: 10c0/ef31cfe06b35017d7855eb3eb3d9c64853e36ea7ad0398cb0754c70c48bfa6abd70d5b7906853877e1ab479c4b01fab3804526eebdfb6adf983ceda35426b16e languageName: node linkType: hard @@ -8085,9 +8085,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.182": - version: 4.17.24 - resolution: "@types/lodash@npm:4.17.24" - checksum: 10c0/b72f60d4daacdad1fa643edb3faba204c02a01eb1ac00a83ff73496a6d236fc55e459c06106e8ced42277dba932d087d8fc090f8de4ef590d3f91e6d6f7ce85a + version: 4.17.16 + resolution: "@types/lodash@npm:4.17.16" + checksum: 10c0/cf017901b8ab1d7aabc86d5189d9288f4f99f19a75caf020c0e2c77b8d4cead4db0d0b842d009b029339f92399f49f34377dd7c2721053388f251778b4c23534 languageName: node linkType: hard @@ -9369,6 +9369,24 @@ __metadata: languageName: node linkType: hard +"@webex/calling@npm:3.11.0-next.15": + version: 3.11.0-next.15 + resolution: "@webex/calling@npm:3.11.0-next.15" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/internal-media-core": "npm:2.23.1" + "@webex/internal-plugin-metrics": "npm:3.11.0-next.8" + "@webex/media-helpers": "npm:3.11.0-next.4" + async-mutex: "npm:0.4.0" + buffer: "npm:6.0.3" + jest-html-reporters: "npm:3.0.11" + platform: "npm:1.3.6" + uuid: "npm:8.3.2" + xstate: "npm:4.30.6" + checksum: 10c0/1b03669ed2f33d7bcde3b5996cd53805f54c1fffed0432e60e864c12e9b54327d8874d03dcbde0e4f05fa6e489ff162bd4425d9d4beb7d41a4c58971de624a2b + languageName: node + linkType: hard + "@webex/cc-components@workspace:*, @webex/cc-components@workspace:packages/contact-center/cc-components": version: 0.0.0-use.local resolution: "@webex/cc-components@workspace:packages/contact-center/cc-components" @@ -9526,7 +9544,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.11.0-next.20" + "@webex/contact-center": "npm:3.12.0-next.1" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" @@ -9897,16 +9915,34 @@ __metadata: languageName: node linkType: hard +"@webex/contact-center@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/contact-center@npm:3.12.0-next.1" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/calling": "npm:3.11.0-next.15" + "@webex/internal-plugin-mercury": "npm:3.11.0-next.10" + "@webex/internal-plugin-metrics": "npm:3.11.0-next.8" + "@webex/internal-plugin-support": "npm:3.11.0-next.11" + "@webex/plugin-authorization": "npm:3.11.0-next.8" + "@webex/plugin-logger": "npm:3.11.0-next.8" + "@webex/webex-core": "npm:3.11.0-next.8" + jest-html-reporters: "npm:3.0.11" + lodash: "npm:^4.17.21" + checksum: 10c0/7b280bd69fdbab3ed1203e21c48080bf3da5a40c290b241eb0d4772da3ab5ebe212d144068b2c48dd678f19eecbfae85152631ab27ff215eb9592dcb407e0c9c + languageName: node + linkType: hard + "@webex/event-dictionary-ts@npm:^1.0.1930": - version: 1.0.2091 - resolution: "@webex/event-dictionary-ts@npm:1.0.2091" + version: 1.0.1947 + resolution: "@webex/event-dictionary-ts@npm:1.0.1947" dependencies: amf-client-js: "npm:^5.2.6" json-schema-to-typescript: "npm:^12.0.0" minimist: "npm:^1.2.8" shelljs: "npm:^0.8.5" webapi-parser: "npm:^0.5.0" - checksum: 10c0/20d0983cebc323593d7a15c8dd26cb7c9d373b0d5e810d6dd818cbb891d42fba844c45fe859ceda810c9daf283fffe939fafcfe0b0068563d34c9731bf8e183e + checksum: 10c0/3b563f15ca895134a7ed24707daf1a937d78e237fe272f5f77e937628b6063e273eb2888d4f14d6a0d8460546d94f9da42819e74142e3d4d931ce7186361fc6c languageName: node linkType: hard @@ -10149,6 +10185,26 @@ __metadata: languageName: node linkType: hard +"@webex/internal-media-core@npm:2.23.1": + version: 2.23.1 + resolution: "@webex/internal-media-core@npm:2.23.1" + 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.10.0" + "@webex/web-client-media-engine": "npm:3.39.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/cd977c3ef4c04ac1aa8a8544fc96a4b30b64c58651dfa07c152499699f33d312ead7385e44d59aaf5a97b24834b934d9ec1776d3b806650c2ce10992515a6f65 + languageName: node + linkType: hard + "@webex/internal-plugin-calendar@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-calendar@npm:2.60.2" @@ -10245,6 +10301,24 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-conversation@npm:3.11.0-next.11": + version: 3.11.0-next.11 + resolution: "@webex/internal-plugin-conversation@npm:3.11.0-next.11" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/helper-html": "npm:3.11.0-next.1" + "@webex/helper-image": "npm:3.11.0-next.1" + "@webex/internal-plugin-encryption": "npm:3.11.0-next.11" + "@webex/internal-plugin-user": "npm:3.11.0-next.8" + "@webex/webex-core": "npm:3.11.0-next.8" + crypto-js: "npm:^4.1.1" + lodash: "npm:^4.17.21" + node-scr: "npm:^0.3.0" + uuid: "npm:^3.3.2" + checksum: 10c0/75801ea25a462679a4495be1f302ac776dee66d75e5347cc03b18607de2f3db468e0f2fbdc5df3207d84565a7259a9bfab59b44fb6779271843879a421f7ef5b + languageName: node + linkType: hard + "@webex/internal-plugin-conversation@npm:3.11.0-next.8": version: 3.11.0-next.8 resolution: "@webex/internal-plugin-conversation@npm:3.11.0-next.8" @@ -10329,6 +10403,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-device@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/internal-plugin-device@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/common-timers": "npm:3.11.0-next.1" + "@webex/http-core": "npm:3.11.0-next.1" + "@webex/internal-plugin-metrics": "npm:3.11.0-next.8" + "@webex/webex-core": "npm:3.11.0-next.8" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/d67b90eb8ad5d6cb6794a72143d32dc2f8d23fdf2284b2a0d09127284ab5252a3a6b92190a2f4a3022cc229d3c11ceabf65529d2b9914ce289869a25a45ba1b3 + languageName: node + linkType: hard + "@webex/internal-plugin-dss@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-dss@npm:3.11.0-next.9" @@ -10421,6 +10512,32 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-encryption@npm:3.11.0-next.11": + version: 3.11.0-next.11 + resolution: "@webex/internal-plugin-encryption@npm:3.11.0-next.11" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/common-timers": "npm:3.11.0-next.1" + "@webex/http-core": "npm:3.11.0-next.1" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/internal-plugin-mercury": "npm:3.11.0-next.10" + "@webex/test-helper-file": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + 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/ce331d6e4ffa9c1766d5acedf94dfa5791a4980e6ecfe92f79d62fa14a0bb04dfa9ea7498712cfcd3cfe5e6f23e630f3615aad7a941b19c38ee4742c00752208 + languageName: node + linkType: hard + "@webex/internal-plugin-encryption@npm:3.11.0-next.8": version: 3.11.0-next.8 resolution: "@webex/internal-plugin-encryption@npm:3.11.0-next.8" @@ -10493,6 +10610,17 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-feature@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/internal-plugin-feature@npm:3.11.0-next.8" + dependencies: + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/webex-core": "npm:3.11.0-next.8" + lodash: "npm:^4.17.21" + checksum: 10c0/090c3eb00ec381d5c9dd56fd3ff846c7130faff73f0caba4c96c9e7e6ac33174b6dc18d3008b3a27319b1d41a92b08bb05fb818f9cee61415f67aa29cb0e6abd + languageName: node + linkType: hard + "@webex/internal-plugin-llm@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-llm@npm:3.11.0-next.9" @@ -10667,6 +10795,30 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-mercury@npm:3.11.0-next.10": + version: 3.11.0-next.10 + resolution: "@webex/internal-plugin-mercury@npm:3.11.0-next.10" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/common-timers": "npm:3.11.0-next.1" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/internal-plugin-feature": "npm:3.11.0-next.8" + "@webex/internal-plugin-metrics": "npm:3.11.0-next.8" + "@webex/test-helper-chai": "npm:3.11.0-next.1" + "@webex/test-helper-mocha": "npm:3.11.0-next.1" + "@webex/test-helper-mock-web-socket": "npm:3.11.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.11.0-next.1" + "@webex/test-helper-refresh-callback": "npm:3.11.0-next.1" + "@webex/test-helper-test-users": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/1bd648d7b1772388918ec054c0edb2198de956bebdcf5c901e1ce838cfa4512ca8cccb3ddb617446c10d91943bb883d45b340e031e98f1c066337ac669842260 + languageName: node + linkType: hard + "@webex/internal-plugin-mercury@npm:3.11.0-next.8": version: 3.11.0-next.8 resolution: "@webex/internal-plugin-mercury@npm:3.11.0-next.8" @@ -10750,6 +10902,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-metrics@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/internal-plugin-metrics@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/common-timers": "npm:3.11.0-next.1" + "@webex/test-helper-chai": "npm:3.11.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + ip-anonymize: "npm:^0.1.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/eb8fa1acf5e5e7886667e15f17db4d49c40db7c71f02f2b2c9113d71a006c71c88fee431fef9e9bebec3bcd149bcc329a13e93c53248d2bd91219d6c34f02265 + languageName: node + linkType: hard + "@webex/internal-plugin-presence@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-presence@npm:2.60.2" @@ -10828,6 +10996,21 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-search@npm:3.11.0-next.11": + version: 3.11.0-next.11 + resolution: "@webex/internal-plugin-search@npm:3.11.0-next.11" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.11.0-next.11" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/internal-plugin-encryption": "npm:3.11.0-next.11" + "@webex/webex-core": "npm:3.11.0-next.8" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/46850372acebb6171df39b14d24b10af7f95ce648478961bf3f17c64ee29367afeca9312ebf779fb8de5d63fbc268d230539f837e864394a61a054cfa19ce783 + languageName: node + linkType: hard + "@webex/internal-plugin-search@npm:3.11.0-next.8": version: 3.11.0-next.8 resolution: "@webex/internal-plugin-search@npm:3.11.0-next.8" @@ -10877,6 +11060,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-support@npm:3.11.0-next.11": + version: 3.11.0-next.11 + resolution: "@webex/internal-plugin-support@npm:3.11.0-next.11" + dependencies: + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/internal-plugin-search": "npm:3.11.0-next.11" + "@webex/test-helper-chai": "npm:3.11.0-next.1" + "@webex/test-helper-file": "npm:3.11.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.11.0-next.1" + "@webex/test-helper-test-users": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/d7fd4632c8fae73b3b9fe29870fec0dbad9a9a64f0d8067ad7db6d62ef31dce2c74060f63886fcc6d316e1542bbd4501d88e52aa279bc6af301f8570b2a0c516 + languageName: node + linkType: hard + "@webex/internal-plugin-support@npm:3.11.0-next.8": version: 3.11.0-next.8 resolution: "@webex/internal-plugin-support@npm:3.11.0-next.8" @@ -10972,6 +11172,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-user@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/internal-plugin-user@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/test-helper-chai": "npm:3.11.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.11.0-next.1" + "@webex/test-helper-test-users": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/24cfa30e8315124ba5db127042797afe998514128e2bf52cdfa02eed68b8e1f4a67194a573861cfde42a2256cd2281f2035e5dd940df12a08c6b7339eebd6ed6 + languageName: node + linkType: hard + "@webex/internal-plugin-voicea@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-voicea@npm:3.11.0-next.9" @@ -11015,6 +11231,17 @@ __metadata: languageName: node linkType: hard +"@webex/media-helpers@npm:3.11.0-next.4": + version: 3.11.0-next.4 + resolution: "@webex/media-helpers@npm:3.11.0-next.4" + dependencies: + "@webex/internal-media-core": "npm:2.23.1" + "@webex/ts-events": "npm:^1.1.0" + "@webex/web-media-effects": "npm:2.33.0" + checksum: 10c0/14483373c32d4eb2c06538a12d099107d2bac42a3be47dd6dcb34d0f55c529a19988809f3e27d5ba46590a17d4eecf1aff4b4eeae3d74a367f9d806b54c06584 + 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" @@ -11133,6 +11360,23 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-browser@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/plugin-authorization-browser@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/plugin-authorization-node": "npm:3.11.0-next.8" + "@webex/storage-adapter-local-storage": "npm:3.11.0-next.8" + "@webex/storage-adapter-spec": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + jsonwebtoken: "npm:^9.0.2" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/f2cc4b9aa882a725eef9d994104b0a6dc3870393d8110ef5e55d2514cf4c1fe5a30737c99e3c541d59babf74e385de21b48b9860fbc04b8ed6644c6eb2a78ec6 + languageName: node + linkType: hard + "@webex/plugin-authorization-node@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization-node@npm:2.60.2" @@ -11172,6 +11416,19 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-node@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/plugin-authorization-node@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/internal-plugin-device": "npm:3.11.0-next.8" + "@webex/webex-core": "npm:3.11.0-next.8" + jsonwebtoken: "npm:^9.0.0" + uuid: "npm:^3.3.2" + checksum: 10c0/8cc84c000e1d846beebd064a9ab5daf1ed3e3463b555cbb35db35249a131df020bbb4e22f3616e664f6ae5bb28fb0126d5dc99a95144db98b06698758bf3c32d + languageName: node + linkType: hard + "@webex/plugin-authorization@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization@npm:2.60.2" @@ -11202,6 +11459,16 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/plugin-authorization@npm:3.11.0-next.8" + dependencies: + "@webex/plugin-authorization-browser": "npm:3.11.0-next.8" + "@webex/plugin-authorization-node": "npm:3.11.0-next.8" + checksum: 10c0/ae2f6b180f61d49788cf0bb2600267b3554db012f59e9364f42c5ecb8d83e75c465f15cbbf3ca50098b8486dcd453aba5c8239a8ce35bba9391d9c331d3047bd + languageName: node + linkType: hard + "@webex/plugin-device-manager@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-device-manager@npm:2.60.2" @@ -11308,6 +11575,20 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-logger@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/plugin-logger@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/test-helper-chai": "npm:3.11.0-next.1" + "@webex/test-helper-mocha": "npm:3.11.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + lodash: "npm:^4.17.21" + checksum: 10c0/980a19e06f8a71c892584e3dff0dcbd83f12715482a2416cb60d373a74ccb38cb20db2eaf52df9d021904b3c585b60179f8775b547b7ee5602e55d664067faed + languageName: node + linkType: hard + "@webex/plugin-meetings@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-meetings@npm:2.60.2" @@ -11776,6 +12057,17 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-local-storage@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/storage-adapter-local-storage@npm:3.11.0-next.8" + dependencies: + "@webex/storage-adapter-spec": "npm:3.11.0-next.1" + "@webex/test-helper-mocha": "npm:3.11.0-next.1" + "@webex/webex-core": "npm:3.11.0-next.8" + checksum: 10c0/510b29ad0652e42a02c2011e5f82e7bf0d9cdb44ecb94e93683d8895b2bb4f6a31d66edeaab9487a18d5d320413a6396f5b78f701997f5f96e11e4a759085164 + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:2.60.2": version: 2.60.2 resolution: "@webex/storage-adapter-spec@npm:2.60.2" @@ -12293,7 +12585,7 @@ __metadata: languageName: node linkType: hard -"@webex/ts-sdp@npm:1.8.2, @webex/ts-sdp@npm:^1.8.1": +"@webex/ts-sdp@npm:1.8.2": version: 1.8.2 resolution: "@webex/ts-sdp@npm:1.8.2" checksum: 10c0/d336f6d3599cbee418de6f02621028266a53c07a0a965e82eb18c9e3993ab9d29d569f5398072de43d9448972776734ca76c1ae2f257859a38793e33f8a519aa @@ -12307,6 +12599,22 @@ __metadata: languageName: node linkType: hard +"@webex/ts-sdp@npm:^1.8.1": + version: 1.8.1 + resolution: "@webex/ts-sdp@npm:1.8.1" + checksum: 10c0/9dc7c63d3274cdbf1cf42c17a2d7bc5afef640bf8200e7c812732c9a19f97d3a84df5bfecba9abc349c19c199ede22c9b7d0db32c1cf802af3d5eb56fda3fefa + languageName: node + linkType: hard + +"@webex/web-capabilities@npm:^1.10.0": + version: 1.10.0 + resolution: "@webex/web-capabilities@npm:1.10.0" + dependencies: + bowser: "npm:^2.11.0" + checksum: 10c0/554fa198ae88249c23035e1e3ef95c939bb26af0582141faa78261c2d364b521e017d018b3fccf2e582455bff9801aea68545a789affac63eb7dd9645513e9ca + languageName: node + linkType: hard + "@webex/web-capabilities@npm:^1.6.1": version: 1.6.1 resolution: "@webex/web-capabilities@npm:1.6.1" @@ -12353,6 +12661,25 @@ __metadata: languageName: node linkType: hard +"@webex/web-client-media-engine@npm:3.39.1": + version: 3.39.1 + resolution: "@webex/web-client-media-engine@npm:3.39.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.10.0" + "@webex/web-media-effects": "npm:2.33.0" + "@webex/webrtc-core": "npm:2.13.5" + async: "npm:^3.2.4" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10c0/26585ae9d354e80c3c852a9b8d36d3b610526213995e1747530d5fd5b62f60f79e0aaefa85581b68507073f055c0e7cab6ad70dd1d5f6adaf1dc91b6c30b6246 + languageName: node + linkType: hard + "@webex/web-media-effects@npm:2.33.0": version: 2.33.0 resolution: "@webex/web-media-effects@npm:2.33.0" @@ -12452,6 +12779,26 @@ __metadata: languageName: node linkType: hard +"@webex/webex-core@npm:3.11.0-next.8": + version: 3.11.0-next.8 + resolution: "@webex/webex-core@npm:3.11.0-next.8" + dependencies: + "@webex/common": "npm:3.11.0-next.1" + "@webex/common-timers": "npm:3.11.0-next.1" + "@webex/http-core": "npm:3.11.0-next.1" + "@webex/storage-adapter-spec": "npm:3.11.0-next.1" + 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/e0c76d9c88bbd1fce7f308bb9a105861b8a1dfefa7d71ebab09236117847da7d78845f6f1611d1e7d672a7b214a42b7c9948eb7b4c60960f98f728a70392fe2f + languageName: node + linkType: hard + "@webex/webrtc-core@npm:2.13.5": version: 2.13.5 resolution: "@webex/webrtc-core@npm:2.13.5" @@ -12920,15 +13267,15 @@ __metadata: linkType: hard "amf-client-js@npm:^5.2.6": - version: 5.10.0 - resolution: "amf-client-js@npm:5.10.0" + version: 5.7.0 + resolution: "amf-client-js@npm:5.7.0" dependencies: - "@aml-org/amf-antlr-parsers": "npm:0.8.34" + "@aml-org/amf-antlr-parsers": "npm:0.8.28" ajv: "npm:6.12.6" avro-js: "npm:1.11.3" bin: amf: bin/amf - checksum: 10c0/50f7b9d546a719df4ddb2ae40dbf3721849c3be9a2544db3bf133c2336cb047a95057bbaf3c89df539d9c6ec0609a4f6ac934cb82e53ca404aa1407f3032a714 + checksum: 10c0/0bea2694b22de128d90696115ea2e716179841d5c076eef8e7ca4dba87f6a24090bac2cb8fb702641b3cc0ea445a000cf5f7260d7a3555ae6c6d3d2502f84909 languageName: node linkType: hard @@ -34202,9 +34549,9 @@ __metadata: linkType: hard "underscore@npm:^1.13.2": - version: 1.13.8 - resolution: "underscore@npm:1.13.8" - checksum: 10c0/6677688daeda30484823e77c0b89ce4dcf29964a77d5a06f37299c007ab4bb1c66a0ff75e0d274620b62a1fe2a6ba29879f8214533ca611d71a1ae504f2bfc9b + version: 1.13.7 + resolution: "underscore@npm:1.13.7" + checksum: 10c0/fad2b4aac48847674aaf3c30558f383399d4fdafad6dd02dd60e4e1b8103b52c5a9e5937e0cc05dacfd26d6a0132ed0410ab4258241240757e4a4424507471cd languageName: node linkType: hard