Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@
"prepare": "husky",
"package-tools": "webex-package-tools"
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it still be "transcripts" because it is not one transcript you get?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature says real time transcript and everywhere we are keeping without s so to make it consistent I changed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say, "feature says", where is this?

I was trying to find the help doc and the help doc says real time transcripts in plural?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked in AgentDesktop Code and contact center announcements for this feature. Let me know if you still want it to be changed.

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<RealTimeTranscriptComponentProps> = ({
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 (
<section className={`real-time-transcript ${className || ''}`.trim()} data-testid="real-time-transcript:root">
<div className="real-time-transcript__content" data-testid="real-time-transcript:live-content">
{sortedEntries.length === 0 ? (
<Text className="real-time-transcript__empty" tagname="div" type="body-midsize-regular">
{EMPTY_TRANSCRIPT_MESSAGE}
</Text>
) : (
<>
{sortedEntries.map((entry) => (
<React.Fragment key={entry.id}>
{entry.event ? (
<Text
className="real-time-transcript__event"
data-testid="real-time-transcript:event"
tagname="div"
type="body-midsize-regular"
>
{entry.event}
{entry.displayTime ? (
<Text className="real-time-transcript__event-time" tagname="span" type="body-midsize-regular">
. {entry.displayTime}
</Text>
) : null}
</Text>
) : null}
<div className="real-time-transcript__item" data-testid="real-time-transcript:item">
<div className="real-time-transcript__avatar-wrap">
<Avatar
className="real-time-transcript__avatar-fallback"
icon-name={entry.avatarUrl || entry.isCustomer ? undefined : 'placeholder-bold'}
src={entry.avatarUrl}
title={formatSpeaker(entry.speaker)}
>
{entry.initials || (entry.isCustomer ? 'CU' : 'YO')}
</Avatar>
</div>
<div className="real-time-transcript__text-block">
<div className="real-time-transcript__meta">
<Text tagname="span" type="body-large-bold">
{formatSpeaker(entry.speaker)}
</Text>
{entry.displayTime ? (
<Text className="real-time-transcript__time" tagname="span" type="body-midsize-regular">
{entry.displayTime}
</Text>
) : null}
</div>
<Text className="real-time-transcript__message" tagname="p" type="body-large-regular">
{entry.message}
</Text>
</div>
</div>
</React.Fragment>
))}
</>
)}
</div>
</section>
);
};

const RealTimeTranscriptComponentWithMetrics = withMetrics(RealTimeTranscriptComponent, 'RealTimeTranscript');

export default RealTimeTranscriptComponentWithMetrics;
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,23 @@ export type TaskListComponentProps = Pick<
> &
Partial<Pick<TaskProps, 'currentTask' | 'taskList'>>;

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.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/contact-center/cc-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,6 +15,7 @@ export {
IncomingTaskComponent,
TaskListComponent,
OutdialCallComponent,
RealTimeTranscriptComponent,
};
export * from './components/StationLogin/constants';
export * from './components/StationLogin/station-login.types';
Expand Down
11 changes: 11 additions & 0 deletions packages/contact-center/cc-components/src/wc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we extending outdial call tests as part of this PR?

Copy link
Copy Markdown
Contributor Author

@Kesari3008 Kesari3008 Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests were failing in the pipeline and in my local as well, not sure why that was happening because there is no change in outdial code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's strange. What if we take a fresh clone of next branch? Do we see the same error there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did pull fresh next branch before adding this and it was still failing for me yesterday. I found it strange too. I can try it again today and if it is not needed, I will remove it

Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(<OutdialCallComponent {...addressBookProps} />);
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(<OutdialCallComponent {...addressBookProps} />);
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'});
});
});
});
});
Loading
Loading