Skip to content
Open
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
30 changes: 15 additions & 15 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.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__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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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-transcript.style.scss';

const formatSpeaker = (speaker?: string) => speaker || 'Unknown';

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 ? (
<div className="real-time-transcript__empty">No live transcript available.</div>
) : (
<>
{sortedEntries[0].event ? (
<div className="real-time-transcript__event" data-testid="real-time-transcript:first-event">
{sortedEntries[0].event}
</div>
) : null}
{sortedEntries.map((entry) => (
<div key={entry.id} className="real-time-transcript__item" data-testid="real-time-transcript:item">
<div className="real-time-transcript__avatar-wrap">
{entry.avatarUrl ? (
<img
src={entry.avatarUrl}
alt={formatSpeaker(entry.speaker)}
className="real-time-transcript__avatar-image"
/>
) : (
<Avatar
className="real-time-transcript__avatar-fallback"
icon-name={entry.isCustomer ? undefined : 'placeholder-bold'}
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">
<span>{formatSpeaker(entry.speaker)}</span>
{entry.displayTime ? <span className="real-time-transcript__time">{entry.displayTime}</span> : null}
</div>
<p className="real-time-transcript__message">{entry.message}</p>
</div>
</div>
))}
</>
)}
</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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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(<RealTimeTranscriptComponent {...defaultProps} />);

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 empty state when there are no transcript entries', () => {
render(<RealTimeTranscriptComponent liveTranscriptEntries={[]} />);
expect(screen.getByText('No live transcript available.')).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion packages/contact-center/cc-widgets/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,6 +13,7 @@ export {
CallControlCAD,
TaskList,
OutdialCall,
RealTimeTranscript,
DigitalChannels,
store,
};
9 changes: 8 additions & 1 deletion packages/contact-center/cc-widgets/src/wc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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, {});

Expand All @@ -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},
];

Expand Down
Loading
Loading