Skip to content

Commit

Permalink
Merge pull request #434 from Cognigy/feature/63342-queue-updates-ui
Browse files Browse the repository at this point in the history
Queue Updates - v3
  • Loading branch information
fabclj authored Jul 9, 2024
2 parents aa6bca5 + 3f7af7c commit 3b8eab3
Show file tree
Hide file tree
Showing 16 changed files with 353 additions and 18 deletions.
89 changes: 89 additions & 0 deletions cypress/e2e/queueUpdates.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />

const getDummyEvent = payload => ({
data: {
_cognigy: {
_webchat3: {
type: "queueUpdate",
payload,
},
},
},
});

describe("Queue Updates", () => {
beforeEach(() => {
cy.visitWebchat().initMockWebchat().openWebchat().startConversation();
});

it("should render queue updates Alternative Text", () => {
cy.receiveMessage(
"",
getDummyEvent({
alternativeText: "Alternative Text",
}),
);

cy.get(".webchat-queue-updates").should("be.visible");
cy.get(".webchat-queue-updates").contains("Alternative Text");
});

it("should render queue updates Position and Wait Time", () => {
cy.receiveMessage(
"",
getDummyEvent({
position: 3,
estimatedWaitTime: 600000,
}),
);

cy.get(".webchat-queue-updates").should("be.visible");
cy.get(".webchat-queue-updates").contains("10 minutes");
});

it("should render LiveAgent Join event and play bell sound", () => {
cy.receiveMessage(
"",
getDummyEvent({
position: 3,
estimatedWaitTime: 600000,
}),
);

cy.get(".webchat-queue-updates").should("be.visible");

cy.receiveMessage(
"Hello!",
{
_cognigy: {
_webchat: {
message: {
text: "Hello!",
},
},
},
},
"agent",
);

// Render ChatEvent chip
cy.get(".webchat-message-row").should("be.visible");
cy.get(".webchat-message-row").contains("Agent joined");

// Play bell sound
cy.get("audio").should(els => {
let audible = false;
els.each((_, el) => {
// console.log(el)
if (el.duration > 0 && el.id === "uifx-813465923") {
audible = true;
}
});
expect(audible).to.eq(true);
});

// Render agent message
cy.get(".webchat-message-row.agent").contains("Hello!");
});
});
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.0",
"@cognigy/chat-components": "^0.27.0",
"@cognigy/socket-client": "5.0.0-beta.10",
"@cognigy/chat-components": "0.28.0",
"@cognigy/socket-client": "5.0.0-beta.17",
"@emotion/cache": "^10.0.29",
"@emotion/react": "^11.7.1",
"@emotion/serialize": "1.1.3",
Expand Down
12 changes: 12 additions & 0 deletions src/common/interfaces/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TWebchat3Event } from "@cognigy/socket-client";

export interface IMessageEvent {
text?: string;
source?: string;
timestamp?: number;
data: {
_cognigy: {
_webchat3: TWebchat3Event;
}
}
}
Binary file added src/webchat-ui/assets/bell-sound.mp3
Binary file not shown.
25 changes: 22 additions & 3 deletions src/webchat-ui/components/WebchatUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ import {
import { HomeScreen } from "./presentational/HomeScreen";
import { PrevConversationsList } from "./presentational/previous-conversations/ConversationsList";
import { PrevConversationsState } from "../../webchat/store/previous-conversations/previous-conversations-reducer";
import { ChatEvent, Message } from "@cognigy/chat-components";
import { ChatEvent, Message, Typography } from "@cognigy/chat-components";
import { isConversationEnded } from "./presentational/previous-conversations/helpers";
import { ISendMessageOptions } from "../../webchat/store/messages/message-middleware";
import { InformationMessage } from "./presentational/InformationMessage";
import { PrivacyNotice } from "./presentational/PrivacyNotice";
import { ChatOptions } from "./presentational/chat-options/ChatOptions";
import QueueUpdates from "./history/QueueUpdates";
import { UIState } from "../../webchat/store/ui/ui-reducer";
import DropZone from "./plugins/input/file/DropZone";
import { IFile } from "../../webchat/store/input/input-reducer";
Expand Down Expand Up @@ -172,6 +173,13 @@ const styleCache = createCache({
stylisPlugins,
});

const TopStatusMessage = styled(Typography)(({ theme }) => ({
display: "block",
textAlign: "center",
padding: "12px",
color: theme.black40
}));

const HistoryWrapper = styled(History)(({ theme }) => ({
overflowY: "auto",
flexGrow: 1,
Expand Down Expand Up @@ -600,7 +608,7 @@ export class WebchatUI extends React.PureComponent<

sendMessage: MessageSender = (...args) => {
if (this.history.current) {
this.history.current.handleScrollTo();
this.history.current.handleScrollTo(undefined, true);
}

this.props.onSendMessage(...args);
Expand Down Expand Up @@ -1165,6 +1173,7 @@ export class WebchatUI extends React.PureComponent<
</h2>
{this.renderHistory()}
</HistoryWrapper>
<QueueUpdates handleScroll={this.history.current?.handleScrollTo} />
{this.renderInput()}
</>
);
Expand Down Expand Up @@ -1314,6 +1323,9 @@ export class WebchatUI extends React.PureComponent<

return (
<>
<TopStatusMessage variant="body-regular" component="div">
You are now talking to an AI agent.
</TopStatusMessage>
{messages.map((message, index) => {
// Lookahead if there is a user reply
const hasReply = messages
Expand Down Expand Up @@ -1345,7 +1357,14 @@ export class WebchatUI extends React.PureComponent<
/>
)}
{enableTypingIndicator && (
<TypingIndicator active={isTyping} delay={messageDelay} direction={config?.settings?.widgetSettings?.sourceDirectionMapping?.bot || "incoming"}/>
<TypingIndicator
active={isTyping}
delay={messageDelay}
direction={
config?.settings?.widgetSettings?.sourceDirectionMapping?.bot ||
"incoming"
}
/>
)}
</>
);
Expand Down
58 changes: 58 additions & 0 deletions src/webchat-ui/components/history/QueueUpdates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { FC, useEffect } from "react";
import styled from "@emotion/styled";
import { Typography } from "@cognigy/chat-components";
import { useSelector } from "../../../webchat/helper/useSelector";
import { formatWaitTime } from "../../utils/formatWaitTime";

const QueueUpdatesWrapper = styled(Typography)(({ theme }) => ({
marginBottom: "var(--webchat-message-margin-block, 24px)",
marginInline: "var(--webchat-message-margin-inline, 20px)",
padding: "12px",
textAlign: "center",
color: theme.black40,
"& span": {
display: "block",
},
}));

interface IQueueUpdatesProps {
handleScroll?: (position?: number, instant?: boolean) => void;
}

const QueueUpdates: FC<IQueueUpdatesProps> = props => {
const queueUpdates = useSelector(state => state.queueUpdates ?? {});
const { handleScroll } = props;

useEffect(() => {
if (queueUpdates?.isQueueActive) {
handleScroll?.(undefined, true);
}
}, [queueUpdates?.isQueueActive]);

if (!queueUpdates?.isQueueActive) return null;

return (
<QueueUpdatesWrapper
variant="body-regular"
id="webchatQueueUpdates"
className="webchat-queue-updates"
component="div"
>
{queueUpdates?.alternativeText ? (
<span>{queueUpdates.alternativeText}</span>
) : (
<>
<span>
Current queue position: <strong>{queueUpdates?.position || ""}</strong>
</span>
<span>
Estimated waiting time:{" "}
<strong>{formatWaitTime(queueUpdates?.estimatedWaitTime)}</strong>
</span>
</>
)}
</QueueUpdatesWrapper>
);
};

export default QueueUpdates;
7 changes: 7 additions & 0 deletions src/webchat-ui/utils/bell-sound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Message notification imports
import UIfx from 'uifx';

const fileUrl = require('../assets/bell-sound.mp3').default as string;
const bellSound = new UIfx(fileUrl);

export default bellSound;
24 changes: 24 additions & 0 deletions src/webchat-ui/utils/formatWaitTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// used on Queue Updates
export const formatWaitTime = (ms: number | null) => {
if (!ms) return "";
const seconds = ms / 1000;

const getMinutes = (minutes: number) => {
return `${minutes} ${minutes > 1 ? "minutes" : "minute"}`;
};

if (seconds < 60) {
return `${seconds} seconds`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return getMinutes(minutes);
}

const hours = Math.floor(seconds / 3600);
const restMinutes = minutes - hours * 60;

return `${hours} ${hours > 1 ? "hours" : "hour"}${
restMinutes > 0 ? ` and ${getMinutes(restMinutes)}` : ""
}`;
};
19 changes: 19 additions & 0 deletions src/webchat/helper/message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@

import { TWebchat3Event } from "@cognigy/socket-client";
import { IMessageEvent } from "../../common/interfaces/event";
import { IMessage } from "../../common/interfaces/message";

const getTextFromMessage = (message: IMessage) => {
Expand Down Expand Up @@ -25,4 +28,20 @@ export const getMessageAttachmentType = (message: IMessage): string => {
return message?.data?._cognigy?._webchat?.message?.attachment?.type || "";
};

export const isQueueUpdate = (message: IMessageEvent): boolean => {
return !!(message?.data?._cognigy?._webchat3?.type === "queueUpdate");
};

export const isEventMessage = (message: IMessageEvent): boolean => {
return !!message?.data?._cognigy?._webchat3?.type;
};

export const getEventPayload = (message: IMessageEvent): TWebchat3Event['payload'] => {
return message?.data?._cognigy?._webchat3?.payload;
};

export const getEventType = (message: IMessageEvent): string => {
return message?.data?._cognigy?._webchat3?.type;
};

export default getTextFromMessage;
25 changes: 24 additions & 1 deletion src/webchat/store/messages/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
setRequestRatingScreenTitle,
} from "../rating/rating-reducer";
import { SocketClient } from "@cognigy/socket-client";
import { getEventPayload, isEventMessage, isQueueUpdate } from "../../helper/message";
import { updateQueueData } from "../queue-updates/slice";
import { IMessageEvent } from "../../../common/interfaces/event";

const RECEIVE_MESSAGE = "RECEIVE_MESSAGE";
export const receiveMessage = (message: IMessage, options: Partial<ISendMessageOptions> = {}) => ({
Expand All @@ -25,6 +28,13 @@ export const receiveMessage = (message: IMessage, options: Partial<ISendMessageO
});
export type ReceiveMessageAction = ReturnType<typeof receiveMessage>;

const RECEIVE_EVENT = "RECEIVE_EVENT";
export const receiveEvent = (event: IMessageEvent) => ({
type: RECEIVE_EVENT as "RECEIVE_EVENT",
event: { ...event } as IMessageEvent,
});
export type ReceiveEventAction = ReturnType<typeof receiveEvent>;

export const createOutputHandler = (store: Store) => output => {
// handle custom webchat actions
if (output.data && output.data._webchat) {
Expand Down Expand Up @@ -74,7 +84,20 @@ export const createOutputHandler = (store: Store) => output => {
}

store.dispatch(setTyping("remove"));
store.dispatch(receiveMessage(output));

// we handle event Messages in a custom way
if (isEventMessage(output?.data)) {
const payload = getEventPayload(output?.data);
if (isQueueUpdate(output?.data)) {
store.dispatch(updateQueueData(payload));
}
// else {
// TODO: implement events logic on middlewares when available from RT
// store.dispatch(receiveEvent(output));
// }
} else {
store.dispatch(receiveMessage(output));
}
};

export const registerMessageHandler = (store: Store, client: SocketClient) => {
Expand Down
Loading

0 comments on commit 3b8eab3

Please sign in to comment.