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,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<RealtimeTranscriptComponentProps> = ({
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 (
<section className={`real-time-transcript ${className || ''}`.trim()} data-testid="real-time-transcript:root">
<div className="real-time-transcript__tabs" role="tablist" aria-label="Conversation transcript tabs">
<button
type="button"
role="tab"
aria-selected={activeTab === 'live'}
className={`real-time-transcript__tab ${activeTab === 'live' ? 'real-time-transcript__tab--active' : ''}`.trim()}
onClick={() => onTabChange?.('live')}
data-testid="real-time-transcript:live-tab"
>
Comment on lines +31 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add an IVR tab trigger to the transcript tablist

The component models two tabs ('ivr' | 'live') and has separate IVR rendering, but the tablist renders only a Live tab and its handler only emits 'live'. In the default state, users have no in-component way to switch to IVR content, so ivrTranscript is effectively inaccessible unless a parent forces activeTab='ivr' externally.

Useful? React with 👍 / 👎.

Live transcript
</button>
</div>

{activeTab === 'ivr' ? (
<div className="real-time-transcript__empty" data-testid="real-time-transcript:ivr-content">
{ivrTranscript || 'No IVR transcript available.'}
</div>
) : (
<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
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ export type TaskListComponentProps = Pick<
> &
Partial<Pick<TaskProps, 'currentTask' | 'taskList'>>;

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.
*/
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
14 changes: 14 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,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);
}
Loading
Loading