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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.task-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;
}

.task-transcript__tabs {
align-items: center;
column-gap: 1.5rem;
display: flex;
margin-bottom: 0.75rem;
}

.task-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;
}

.task-transcript__tab--active {
border-bottom-color: var(--mds-color-theme-text-primary-normal);
color: var(--mds-color-theme-text-primary-normal);
font-weight: 700;
}

.task-transcript__content {
display: flex;
flex: 1;
flex-direction: column;
overflow-y: auto;
row-gap: 0.875rem;
}

.task-transcript__event {
color: var(--mds-color-theme-text-secondary-normal);
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
}

.task-transcript__item {
align-items: flex-start;
column-gap: 0.625rem;
display: flex;
}

.task-transcript__avatar-wrap {
flex-shrink: 0;
height: 1.75rem;
width: 1.75rem;
}

.task-transcript__avatar-image {
border-radius: 50%;
display: block;
height: 100%;
object-fit: cover;
width: 100%;
}

.task-transcript__avatar-fallback {
--mdc-avatar-size: 1.75rem;
}

.task-transcript__text-block {
min-width: 0;
}

.task-transcript__meta {
color: var(--mds-color-theme-text-secondary-normal);
display: flex;
font-size: 0.75rem;
line-height: 1rem;
}

.task-transcript__time {
color: #2e6de5;
margin-left: 0.5rem;
text-decoration: underline;
}

.task-transcript__message {
color: var(--mds-color-theme-text-primary-normal);
font-size: 1.0625rem;
line-height: 1.5rem;
margin: 0.125rem 0 0;
}

.task-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,99 @@
import React, {useMemo} from 'react';
import {Avatar} from '@momentum-design/components/dist/react';
import {withMetrics} from '@webex/cc-ui-logging';
import {TaskTranscriptComponentProps} from '../task.types';
import './styles.scss';

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

const TaskTranscriptComponent: React.FC<TaskTranscriptComponentProps> = ({
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={`task-transcript ${className || ''}`.trim()} data-testid="task-transcript:root">
<div className="task-transcript__tabs" role="tablist" aria-label="Conversation transcript tabs">
<button
type="button"
role="tab"
aria-selected={activeTab === 'ivr'}
className={`task-transcript__tab ${activeTab === 'ivr' ? 'task-transcript__tab--active' : ''}`.trim()}
onClick={() => onTabChange?.('ivr')}

Choose a reason for hiding this comment

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

P1 Badge Persist transcript tab selection on tab click

The tab buttons only invoke onTabChange and never update any internal state, while activeTab is defaulted to 'live'. In the default widget flow (TaskTranscript + useTaskTranscript), this means clicking “IVR transcript” does not actually switch the rendered content unless the host app builds its own controlled-state loop; for typical usage (especially plain web-component embedding), the IVR tab is effectively unusable.

Useful? React with 👍 / 👎.

data-testid="task-transcript:ivr-tab"
>
IVR transcript
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'live'}
className={`task-transcript__tab ${activeTab === 'live' ? 'task-transcript__tab--active' : ''}`.trim()}
onClick={() => onTabChange?.('live')}
data-testid="task-transcript:live-tab"
>
Live transcript
</button>
</div>

{activeTab === 'ivr' ? (
<div className="task-transcript__empty" data-testid="task-transcript:ivr-content">
{ivrTranscript || 'No IVR transcript available.'}
</div>
) : (
<div className="task-transcript__content" data-testid="task-transcript:live-content">
{sortedEntries.length === 0 ? (
<div className="task-transcript__empty">No live transcript available.</div>
) : (
<>
{sortedEntries[0].event ? (
<div className="task-transcript__event" data-testid="task-transcript:first-event">
{sortedEntries[0].event}
</div>
) : null}
{sortedEntries.map((entry) => (
<div key={entry.id} className="task-transcript__item" data-testid="task-transcript:item">
<div className="task-transcript__avatar-wrap">
{entry.avatarUrl ? (
<img src={entry.avatarUrl} alt={formatSpeaker(entry.speaker)} className="task-transcript__avatar-image" />

Check failure on line 69 in packages/contact-center/cc-components/src/components/task/TaskTranscript/task-transcript.tsx

View workflow job for this annotation

GitHub Actions / linter

Replace `·src={entry.avatarUrl}·alt={formatSpeaker(entry.speaker)}·className="task-transcript__avatar-image"` with `⏎························src={entry.avatarUrl}⏎························alt={formatSpeaker(entry.speaker)}⏎························className="task-transcript__avatar-image"⏎·····················`
) : (
<Avatar
className="task-transcript__avatar-fallback"
icon-name={entry.isCustomer ? undefined : 'placeholder-bold'}
title={formatSpeaker(entry.speaker)}
>
{entry.initials || (entry.isCustomer ? 'CU' : 'YO')}
</Avatar>
)}
</div>
<div className="task-transcript__text-block">
<div className="task-transcript__meta">
<span>{formatSpeaker(entry.speaker)}</span>
{entry.displayTime ? <span className="task-transcript__time">{entry.displayTime}</span> : null}
</div>
<p className="task-transcript__message">{entry.message}</p>
</div>
</div>
))}
</>
)}
</div>
)}
</section>
);
};

const TaskTranscriptComponentWithMetrics = withMetrics(TaskTranscriptComponent, 'TaskTranscript');

export default TaskTranscriptComponentWithMetrics;
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 TaskTranscriptEntry {
id: string;
speaker: string;
message: string;
timestamp: number;
displayTime?: string;
event?: string;
isCustomer?: boolean;
avatarUrl?: string;
initials?: string;
}

export interface TaskTranscriptComponentProps {
ivrTranscript?: string;
liveTranscriptEntries?: TaskTranscriptEntry[];
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 TaskTranscriptComponent from './components/task/TaskTranscript/task-transcript';

export {
UserStateComponent,
Expand All @@ -14,6 +15,7 @@ export {
IncomingTaskComponent,
TaskListComponent,
OutdialCallComponent,
TaskTranscriptComponent,
};
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 TaskTranscriptComponent from './components/task/TaskTranscript/task-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 WebTaskTranscriptComponent = r2wc(TaskTranscriptComponent, {
props: {
ivrTranscript: 'string',
liveTranscriptEntries: 'json',
activeTab: 'string',
onTabChange: 'function',
className: 'string',
},
});
if (!customElements.get('component-cc-task-transcript')) {
customElements.define('component-cc-task-transcript', WebTaskTranscriptComponent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import {fireEvent, render, screen} from '@testing-library/react';
import '@testing-library/jest-dom';
import TaskTranscriptComponent from '../../../../src/components/task/TaskTranscript/task-transcript';
import {TaskTranscriptComponentProps} from '../../../../src/components/task/task.types';

describe('TaskTranscriptComponent', () => {
const defaultProps: TaskTranscriptComponentProps = {
ivrTranscript: 'IVR summary text',
activeTab: 'live',
liveTranscriptEntries: [
{
id: '2',
speaker: '%Customer%',
message: 'Customer message',
timestamp: 2,
displayTime: '00:02',
isCustomer: true,
},
{
id: '1',
speaker: '%You%',
message: 'Agent message',
timestamp: 1,
displayTime: '00:01',
},
],
};

it('renders live transcript entries and sorts by timestamp', () => {
render(<TaskTranscriptComponent {...defaultProps} />);

const messages = screen.getAllByTestId('task-transcript:item');
expect(messages).toHaveLength(2);
expect(messages[0]).toHaveTextContent('Agent message');
expect(messages[1]).toHaveTextContent('Customer message');
});

it('renders ivr content when ivr tab active', () => {
render(<TaskTranscriptComponent {...defaultProps} activeTab="ivr" />);
expect(screen.getByTestId('task-transcript:ivr-content')).toHaveTextContent('IVR summary text');
});

it('calls onTabChange when tabs clicked', () => {
const onTabChange = jest.fn();
render(<TaskTranscriptComponent {...defaultProps} onTabChange={onTabChange} />);

fireEvent.click(screen.getByTestId('task-transcript:ivr-tab'));
expect(onTabChange).toHaveBeenCalledWith('ivr');
});
});
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, TaskTranscript} 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,
TaskTranscript,
DigitalChannels,
store,
};
12 changes: 11 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, TaskTranscript} from '@webex/cc-task';
import {DigitalChannels} from '@webex/cc-digital-channels';

const WebUserState = r2wc(UserState, {
Expand Down Expand Up @@ -53,6 +53,15 @@ const WebCallControlCAD = r2wc(CallControlCAD, {
});

const WebOutdialCall = r2wc(OutdialCall, {});
const WebTaskTranscript = r2wc(TaskTranscript, {
props: {
ivrTranscript: 'string',
liveTranscriptEntries: 'json',
activeTab: 'string',
onTabChange: 'function',
className: 'string',
},
});

const WebDigitalChannels = r2wc(DigitalChannels, {});

Expand All @@ -66,6 +75,7 @@ const components = [
{name: 'widget-cc-call-control', component: WebCallControl},
{name: 'widget-cc-outdial-call', component: WebOutdialCall},
{name: 'widget-cc-call-control-cad', component: WebCallControlCAD},
{name: 'widget-cc-task-transcript', component: WebTaskTranscript},
{name: 'widget-cc-digital-channels', component: WebDigitalChannels},
];

Expand Down
Loading
Loading