diff --git a/package.json b/package.json index 0988a96c3..f284d38a8 100644 --- a/package.json +++ b/package.json @@ -72,4 +72,4 @@ "prepare": "husky", "package-tools": "webex-package-tools" } -} \ No newline at end of file +} diff --git a/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.style.scss b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.style.scss new file mode 100644 index 000000000..d3cb6fa38 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.style.scss @@ -0,0 +1,75 @@ +.real-time-transcript { + background: var(--mds-color-theme-background-primary-normal); + border-radius: 0.5rem; + display: flex; + flex-direction: column; + min-height: 12rem; + padding: 1rem 1.125rem; +} + +.real-time-transcript__content { + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; + row-gap: 1.25rem; +} + +.real-time-transcript__event { + color: var(--mds-color-theme-text-secondary-normal); + font-size: 0.75rem; + line-height: 1rem; + margin: 0.25rem 0 0.125rem; + text-align: center; +} + +.real-time-transcript__event-time { + color: inherit; +} + +.real-time-transcript__item { + align-items: flex-start; + column-gap: 0.875rem; + display: flex; +} + +.real-time-transcript__avatar-wrap { + flex-shrink: 0; + height: 2.25rem; + width: 2.25rem; +} + +.real-time-transcript__avatar-fallback { + --mdc-avatar-size: 2.25rem; +} + +.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.625rem; + 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.25rem 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/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..d104740cc --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/RealTimeTranscript/real-time-transcript.tsx @@ -0,0 +1,87 @@ +import React, {useMemo} from 'react'; +import {Avatar, Text} from '@momentum-design/components/dist/react'; +import {withMetrics} from '@webex/cc-ui-logging'; +import {RealTimeTranscriptComponentProps} from '../task.types'; +import './real-time-transcript.style.scss'; + +const formatSpeaker = (speaker?: string) => speaker || 'Unknown'; +const EMPTY_TRANSCRIPT_MESSAGE = 'No live transcript available.'; + +const RealTimeTranscriptComponent: React.FC = ({ + liveTranscriptEntries = [], + className, +}) => { + const sortedEntries = useMemo( + () => + [...liveTranscriptEntries].sort((a, b) => { + if (a.timestamp === b.timestamp) return 0; + return a.timestamp > b.timestamp ? 1 : -1; + }), + [liveTranscriptEntries] + ); + + return ( +
+
+ {sortedEntries.length === 0 ? ( + + {EMPTY_TRANSCRIPT_MESSAGE} + + ) : ( + <> + {sortedEntries.map((entry) => ( + + {entry.event ? ( + + {entry.event} + {entry.displayTime ? ( + + . {entry.displayTime} + + ) : null} + + ) : null} +
+
+ + {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/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 0b7c33765..3816154d3 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,23 @@ export type TaskListComponentProps = Pick< > & Partial>; +export interface RealTimeTranscriptEntry { + id: string; + speaker: string; + message: string; + timestamp: number; + displayTime?: string; + event?: string; + isCustomer?: boolean; + avatarUrl?: string; + initials?: string; +} + +export interface RealTimeTranscriptComponentProps { + liveTranscriptEntries?: RealTimeTranscriptEntry[]; + 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..a7df5d896 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..0e22bc800 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,13 @@ 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: { + liveTranscriptEntries: 'json', + 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/OutdialCall/out-dial-call.tsx b/packages/contact-center/cc-components/tests/components/task/OutdialCall/out-dial-call.tsx index 219a4677e..e35b09862 100644 --- a/packages/contact-center/cc-components/tests/components/task/OutdialCall/out-dial-call.tsx +++ b/packages/contact-center/cc-components/tests/components/task/OutdialCall/out-dial-call.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {render, fireEvent, screen, waitFor, within} from '@testing-library/react'; +import {act, render, fireEvent, screen, waitFor, within} from '@testing-library/react'; import '@testing-library/jest-dom'; import OutdialCallComponent from '../../../../src/components/task/OutdialCall/outdial-call'; import {KEY_LIST} from '../../../../src/components/task/OutdialCall/constants'; @@ -1089,71 +1089,93 @@ describe('OutdialCallComponent', () => { }); it('maintains scroll pagination with search', async () => { - const mockGetAddressBook = jest - .fn() - .mockResolvedValueOnce({ - data: Array.from({length: 25}, (_, i) => ({ - id: `${i}`, - name: `John ${i}`, - number: `+1469000${i}`, - })), - total: 50, - }) - .mockResolvedValueOnce({ - data: Array.from({length: 25}, (_, i) => ({ - id: `${i + 25}`, - name: `John ${i + 25}`, - number: `+1469000${i + 25}`, - })), - total: 50, + jest.useFakeTimers(); + try { + const mockGetAddressBook = jest + .fn() + .mockResolvedValueOnce({ + data: Array.from({length: 25}, (_, i) => ({ + id: `${i}`, + name: `Contact ${i}`, + number: `+1469000${i}`, + })), + total: 50, + }) + .mockResolvedValueOnce({ + data: Array.from({length: 25}, (_, i) => ({ + id: `search-${i}`, + name: `John ${i}`, + number: `+1469100${i}`, + })), + total: 50, + }) + .mockResolvedValueOnce({ + data: Array.from({length: 25}, (_, i) => ({ + id: `search-${i + 25}`, + name: `John ${i + 25}`, + number: `+1469000${i + 25}`, + })), + total: 50, + }); + + const addressBookProps: OutdialCallComponentProps = { + ...props, + isAddressBookEnabled: true, + getAddressBookEntries: mockGetAddressBook, + }; + + const {container} = render(); + const tabList = await waitFor(() => getTabList(container)); + const tabs = within(tabList as HTMLElement).getAllByRole('tab'); + const addressBookTab = tabs[0]; + fireEvent.click(addressBookTab); + + await waitFor(() => { + expect(screen.getByText('Contact 0')).toBeInTheDocument(); }); - const addressBookProps: OutdialCallComponentProps = { - ...props, - isAddressBookEnabled: true, - getAddressBookEntries: mockGetAddressBook, - }; + // Search + const searchInput = await screen.findByTestId('outdial-address-book-search-input'); + const searchEvent = new Event('input', {bubbles: true}); + Object.defineProperty(searchEvent, 'target', { + writable: false, + value: {value: 'John'}, + }); + fireEvent(searchInput, searchEvent); - const {container} = render(); - const tabList = await waitFor(() => getTabList(container)); - const tabs = within(tabList as HTMLElement).getAllByRole('tab'); - const addressBookTab = tabs[0]; - fireEvent.click(addressBookTab); + await act(async () => { + jest.advanceTimersByTime(500); + }); - await waitFor(() => { - expect(screen.getByText('John 0')).toBeInTheDocument(); - }); + // Wait for debounced search + await waitFor( + () => { + expect(mockGetAddressBook).toHaveBeenCalledWith({page: 0, pageSize: 25, search: 'John'}); + }, + {timeout: 1000} + ); - // Search - const searchInput = await screen.findByTestId('outdial-address-book-search-input'); - const searchEvent = new Event('input', {bubbles: true}); - Object.defineProperty(searchEvent, 'target', { - writable: false, - value: {value: 'John'}, - }); - fireEvent(searchInput, searchEvent); + await waitFor(() => { + expect(screen.getByText('John 0')).toBeInTheDocument(); + }); - // Wait for debounced search - await waitFor( - () => { - expect(mockGetAddressBook).toHaveBeenCalledWith({page: 0, pageSize: 25, search: 'John'}); - }, - {timeout: 1000} - ); + // Trigger load more with search term + const mockEntry = { + isIntersecting: true, + target: container.querySelector('.address-book-observer'), + } as IntersectionObserverEntry; - // Trigger load more with search term - const mockEntry = { - isIntersecting: true, - target: container.querySelector('.address-book-observer'), - } as IntersectionObserverEntry; + if (intersectionCallback) { + intersectionCallback([mockEntry], {} as IntersectionObserver); + } - if (intersectionCallback) { - intersectionCallback([mockEntry], {} as IntersectionObserver); + await waitFor(() => { + expect(mockGetAddressBook).toHaveBeenCalledWith({page: 1, pageSize: 25, search: 'John'}); + }); + } finally { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); } - - await waitFor(() => { - expect(mockGetAddressBook).toHaveBeenCalledWith({page: 1, pageSize: 25, search: 'John'}); - }); }); }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/__snapshots__/realtime-transcript.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/__snapshots__/realtime-transcript.snapshot.tsx.snap new file mode 100644 index 000000000..ff8955cec --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/__snapshots__/realtime-transcript.snapshot.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RealTimeTranscriptComponent snapshots matches snapshot with transcript content 1`] = ` +
+
+
+
+
+ + YO + +
+
+
+ + You + + + 11:26 AM + +
+ + Hello there + +
+
+
+
+ + CU + +
+
+
+ + Customer + + + 11:27 AM + +
+ + Hi + +
+
+
+
+
+`; diff --git a/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.snapshot.tsx new file mode 100644 index 000000000..ab266aa72 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.snapshot.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import RealTimeTranscriptComponent from '../../../../src/components/task/RealTimeTranscript/real-time-transcript'; + +describe('RealTimeTranscriptComponent snapshots', () => { + it('matches snapshot with transcript content', () => { + const {container} = render( + + ); + + expect(container).toMatchSnapshot(); + }); +}); 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..670d064ab --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/RealtimeTranscript/realtime-transcript.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {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 = { + 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 transcript event inline with timestamp', () => { + render( + + ); + + expect(screen.getByTestId('real-time-transcript:event')).toHaveTextContent( + '%Tombstone - action occurred%. 11:26 AM' + ); + }); + + it('renders empty state when there are no transcript entries', () => { + render(); + expect(screen.getByText('No live transcript available.')).toBeInTheDocument(); + }); +}); diff --git a/packages/contact-center/cc-widgets/src/index.ts b/packages/contact-center/cc-widgets/src/index.ts index ea1562e11..9d5afaffb 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..cdfd850a4 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,12 @@ const WebCallControlCAD = r2wc(CallControlCAD, { }); const WebOutdialCall = r2wc(OutdialCall, {}); +const WebRealTimeTranscript = r2wc(RealTimeTranscript, { + props: { + liveTranscriptEntries: 'json', + className: 'string', + }, +}); const WebDigitalChannels = r2wc(DigitalChannels, {}); @@ -66,6 +72,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..4bced8ddd 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -13,6 +13,7 @@ import { AgentLoginProfile, LoginOptions, WithWebex, + RealTimeTranscriptionData, } from './store.types'; import {getFeatureFlags} from './util'; @@ -51,6 +52,7 @@ class Store implements IStore { isMuted: boolean = false; isDigitalChannelsInitialized: boolean = false; dataCenter: string = ''; + realtimeTranscriptionData: Partial[] = []; 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..116aa574c 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -95,6 +95,29 @@ 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 = { + agentId: string; + data: RealTimeTranscriptionData; + notifDetails: { + actionEvent?: string; + }; + notifType: string; + orgId: string; +}; + interface IStore { featureFlags: {[key: string]: boolean}; teams: Team[]; @@ -128,6 +151,7 @@ interface IStore { isAddressBookEnabled: boolean; isDigitalChannelsInitialized: boolean; dataCenter: string; + realtimeTranscriptionData: Partial[]; init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise; registerCC(webex?: WithWebex['webex']): Promise; } @@ -207,6 +231,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 +344,8 @@ export type { PaginatedListParams, FetchPaginatedList, TransformPaginatedData, + RealTimeTranscriptionData, + RealTimeTranscriptionEventPayload, }; export { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 87aeb93f4..0b53cc974 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,7 @@ class StoreWrapper implements IStoreWrapper { onTaskAssigned?: (task: ITask) => void; onTaskSelected?: (task: ITask, isClicked: boolean) => void; onErrorCallback?: (widgetName: string, error: Error) => void; + private realtimeTranscriptionListeners: Record void> = {}; constructor() { this.store = Store.getInstance(); @@ -144,6 +146,10 @@ class StoreWrapper implements IStoreWrapper { return this.store.dataCenter; } + get realtimeTranscriptionData() { + return this.store.realtimeTranscriptionData; + } + setDataCenter = (value: string): void => { this.store.dataCenter = value; }; @@ -231,7 +237,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 +425,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 +463,12 @@ class StoreWrapper implements IStoreWrapper { } runInAction(() => { + if (taskToRemove) { + const removedTaskId = taskToRemove.data?.interactionId; + if (removedTaskId && this.store.currentTask?.data?.interactionId === removedTaskId) { + this.store.realtimeTranscriptionData = []; + } + } if (taskToRemove && this.store.currentTask?.data.interactionId === taskToRemove.data.interactionId) { this.setCurrentTask(null); } @@ -599,6 +616,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: RealTimeTranscriptionEventPayload) => + 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 +730,39 @@ class StoreWrapper implements IStoreWrapper { } }; + handleRealtimeTranscription = (payload: RealTimeTranscriptionEventPayload) => { + const transcriptData = payload.data; + if (!transcriptData?.messageId) return; + + const content = transcriptData.content || ''; + if (!content) return; + + const role = transcriptData.role.toUpperCase(); + const publishTimestampRaw = transcriptData.publishTimestamp; + const publishTimestamp = + typeof publishTimestampRaw === 'number' + ? publishTimestampRaw + : Number.parseInt(`${publishTimestampRaw || Date.now()}`, 10); + const normalizedPublishTimestamp = Number.isNaN(publishTimestamp) ? Date.now() : publishTimestamp; + + runInAction(() => { + const transcriptLines = this.store.realtimeTranscriptionData || []; + const newTranscriptData = { + ...transcriptData, + role, + content, + publishTimestamp: normalizedPublishTimestamp, + }; + const hasExistingLine = transcriptLines.some((line) => line.messageId === transcriptData.messageId); + + this.store.realtimeTranscriptionData = hasExistingLine + ? transcriptLines.map((line) => + line.messageId === transcriptData.messageId ? {...line, ...newTranscriptData} : line + ) + : [...transcriptLines, newTranscriptData]; + }); + }; + getBuddyAgents = async ( mediaType: string = this.currentTask.data.interaction.mediaType ): Promise> => { @@ -801,6 +859,8 @@ class StoreWrapper implements IStoreWrapper { this.setConsultStartTimeStamp(undefined); this.setTeamId(''); this.setDigitalChannelsInitialized(false); + this.store.realtimeTranscriptionData = []; + this.realtimeTranscriptionListeners = {}; }); }; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index e05584ed2..df2d54fb3 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -531,6 +531,7 @@ describe('storeEventsWrapper', () => { beforeEach(() => { jest.clearAllMocks(); + storeWrapper['store'].realtimeTranscriptionData = []; // mock return the task list from cc.taskManager }); @@ -780,6 +781,94 @@ describe('storeEventsWrapper', () => { expect(setCallControlAudioSpy).toHaveBeenCalledWith(new MediaStream([mockTrack])); }); + it('should append and then replace realtime transcript content by messageId', () => { + storeWrapper.handleRealtimeTranscription({ + agentId: 'agent-1', + data: { + content: 'Hello', + conversationId: 'conversation-1', + isFinal: false, + messageId: 'message-1', + orgId: 'org-1', + publishTimestamp: 101, + role: 'caller', + trackingId: 'tracking-1', + utteranceId: 'utterance-1', + }, + notifDetails: {actionEvent: 'REAL_TIME_TRANSCRIPTION'}, + notifType: 'REAL_TIME_TRANSCRIPTION', + orgId: 'org-1', + }); + + expect(storeWrapper['store'].realtimeTranscriptionData).toEqual([ + expect.objectContaining({ + content: 'Hello', + isFinal: false, + messageId: 'message-1', + publishTimestamp: 101, + role: 'CALLER', + }), + ]); + + storeWrapper.handleRealtimeTranscription({ + agentId: 'agent-1', + data: { + content: 'Hello there', + conversationId: 'conversation-1', + isFinal: true, + messageId: 'message-1', + orgId: 'org-1', + publishTimestamp: 102, + role: 'caller', + trackingId: 'tracking-1', + utteranceId: 'utterance-1', + }, + notifDetails: {actionEvent: 'REAL_TIME_TRANSCRIPTION'}, + notifType: 'REAL_TIME_TRANSCRIPTION', + orgId: 'org-1', + }); + + expect(storeWrapper['store'].realtimeTranscriptionData).toEqual([ + expect.objectContaining({ + content: 'Hello there', + isFinal: true, + messageId: 'message-1', + publishTimestamp: 102, + role: 'CALLER', + }), + ]); + }); + + it('should accept wrapped realtime transcript event payloads', () => { + storeWrapper.handleRealtimeTranscription({ + agentId: 'agent-2', + data: { + content: 'Agent speaking', + conversationId: 'conversation-2', + isFinal: false, + messageId: 'message-2', + orgId: 'org-2', + publishTimestamp: '201', + role: 'agent', + trackingId: 'tracking-2', + utteranceId: 'utterance-2', + }, + notifDetails: {actionEvent: 'REAL_TIME_TRANSCRIPTION'}, + notifType: 'REAL_TIME_TRANSCRIPTION', + orgId: 'org-2', + }); + + expect(storeWrapper['store'].realtimeTranscriptionData).toEqual([ + expect.objectContaining({ + content: 'Agent speaking', + isFinal: false, + messageId: 'message-2', + publishTimestamp: 201, + role: 'AGENT', + }), + ]); + }); + it('should handle task removal', () => { const refreshTaskListSpy = jest.spyOn(storeWrapper, 'refreshTaskList'); const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); diff --git a/packages/contact-center/task/src/RealTimeTranscript/index.tsx b/packages/contact-center/task/src/RealTimeTranscript/index.tsx new file mode 100644 index 000000000..d10d1e226 --- /dev/null +++ b/packages/contact-center/task/src/RealTimeTranscript/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, realtimeTranscriptionData} = store; + const result = useRealTimeTranscript({ + ...props, + currentTaskId: currentTask?.data?.interactionId, + realtimeTranscriptionData, + }); + 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..b06730042 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,32 @@ import {OutdialAniEntriesResponse} from '@webex/contact-center/dist/types/servic const ENGAGED_LABEL = 'ENGAGED'; const ENGAGED_USERNAME = 'Engaged'; +const getTranscriptSpeaker = (role?: string): string => { + const normalizedRole = role?.toUpperCase(); + if (normalizedRole === 'AGENT') return 'You'; + if (normalizedRole === 'CUSTOMER' || normalizedRole === 'CALLER') return 'Customer'; + + return normalizedRole || 'Unknown'; +}; + +const mapTranscriptLineToEntry = ( + transcriptionData: Partial, + currentTaskId: string +): RealTimeTranscriptEntry => { + const speaker = getTranscriptSpeaker(transcriptionData.role); + const timestamp = + typeof transcriptionData.publishTimestamp === 'number' ? transcriptionData.publishTimestamp : Date.now(); + + return { + id: `${currentTaskId}-${transcriptionData.messageId}`, + speaker, + message: transcriptionData.content, + timestamp, + displayTime: new Date(timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}), + isCustomer: speaker === 'Customer', + }; +}; + // Hook for managing the task list export const useTaskList = (props: UseTaskListProps) => { const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; @@ -146,6 +175,25 @@ export const useTaskList = (props: UseTaskListProps) => { return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; }; +export const useRealTimeTranscript = (props: UseRealTimeTranscriptInternalProps) => { + const {liveTranscriptEntries = [], className, currentTaskId, realtimeTranscriptionData = []} = props; + const mappedRealtimeEntries = useMemo(() => { + if (!currentTaskId) return liveTranscriptEntries; + + const transcriptLines = realtimeTranscriptionData; + if (!transcriptLines.length) { + return liveTranscriptEntries; + } + + return transcriptLines.map((line) => mapTranscriptLineToEntry(line, currentTaskId)); + }, [currentTaskId, realtimeTranscriptionData, liveTranscriptEntries]); + + return { + liveTranscriptEntries: mappedRealtimeEntries, + className, + }; +}; + export const useIncomingTask = (props: UseTaskProps) => { const {onAccepted, onRejected, deviceType, incomingTask, logger} = props; const isBrowser = deviceType === 'BROWSER'; @@ -1061,7 +1109,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..1de9c8832 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 './RealTimeTranscript'; +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..d2c35e5fa 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -1,4 +1,11 @@ -import {TaskProps, ControlProps, OutdialCallProps} from '@webex/cc-components'; +import { + TaskProps, + ControlProps, + OutdialCallProps, + RealTimeTranscriptComponentProps, + RealTimeTranscriptEntry, +} from '@webex/cc-components'; +import {RealTimeTranscriptionData} from '@webex/cc-store'; export type UseTaskProps = Pick & Partial>; @@ -10,6 +17,13 @@ export type IncomingTaskProps = Pick & Partial>; +export type RealTimeTranscriptProps = Pick; + +export type UseRealTimeTranscriptInternalProps = RealTimeTranscriptProps & { + currentTaskId?: string; + realtimeTranscriptionData?: Partial[]; +}; + export type CallControlProps = Partial< Pick< ControlProps, @@ -32,6 +46,8 @@ export type useCallControlProps = Pick< Partial>; export type useOutdialCallProps = Pick; + +export type {RealTimeTranscriptEntry}; 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..a05e6c044 --- /dev/null +++ b/packages/contact-center/task/tests/RealtimeTranscript/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import * as helper from '../../src/helper'; +import store from '@webex/cc-store'; +import {RealTimeTranscript} from '../../src/RealTimeTranscript'; + +jest.mock('@webex/cc-store', () => ({ + currentTask: { + data: { + interactionId: 'test-interaction-id', + }, + }, + realtimeTranscriptionData: [], + 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 = { + liveTranscriptEntries: [{id: '1', speaker: 'Agent', message: 'Hello', timestamp: 1}], + }; + + render(); + + expect(spy).toHaveBeenCalledWith({ + ...transcriptProps, + currentTaskId: 'test-interaction-id', + realtimeTranscriptionData: [], + }); + 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..14a7dfbe0 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 c28bac9ba..a85a4606a 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 @@ -8279,9 +8279,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 @@ -9563,6 +9563,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" @@ -9720,7 +9738,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" @@ -10069,16 +10087,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 @@ -10274,6 +10310,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.4": version: 2.60.4 resolution: "@webex/internal-plugin-calendar@npm:2.60.4" @@ -10338,6 +10394,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" @@ -10406,6 +10480,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" @@ -10472,6 +10563,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" @@ -10532,6 +10649,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" @@ -10651,6 +10779,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" @@ -10721,6 +10873,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.4": version: 2.60.4 resolution: "@webex/internal-plugin-presence@npm:2.60.4" @@ -10768,6 +10936,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" @@ -10800,6 +10983,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" @@ -10879,6 +11079,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" @@ -10922,6 +11138,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" @@ -11004,6 +11231,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.4": version: 2.60.4 resolution: "@webex/plugin-authorization-node@npm:2.60.4" @@ -11030,6 +11274,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.4": version: 2.60.4 resolution: "@webex/plugin-authorization@npm:2.60.4" @@ -11050,6 +11307,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.4": version: 2.60.4 resolution: "@webex/plugin-device-manager@npm:2.60.4" @@ -11124,6 +11391,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.4": version: 2.60.4 resolution: "@webex/plugin-meetings@npm:2.60.4" @@ -11448,6 +11729,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.4": version: 2.60.4 resolution: "@webex/storage-adapter-spec@npm:2.60.4" @@ -11853,7 +12145,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 @@ -11867,6 +12159,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" @@ -11913,6 +12221,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" @@ -11990,6 +12317,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" @@ -12461,15 +12808,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 @@ -33826,9 +34173,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