Skip to content

Commit

Permalink
Add escalation confirm ux
Browse files Browse the repository at this point in the history
  • Loading branch information
peterszerzo committed Jul 24, 2024
1 parent 6459f66 commit 9cf1af5
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 92 deletions.
210 changes: 118 additions & 92 deletions packages/journey-manager/src/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { type Client } from "@nlxai/multimodal";
import { autoUpdate, platform } from "@floating-ui/dom";
import { render, type FunctionComponent, type VNode } from "preact";
import {
render,
type FunctionComponent as FC,
type ComponentChildren,
} from "preact";
import {
useEffect,
useState,
Expand All @@ -11,6 +15,14 @@ import {
type MutableRef,
} from "preact/hooks";
import { createPortal } from "preact/compat";
import {
ArrowBackIcon,
CallEndIcon,
CheckIcon,
CloseIcon,
MultimodalIcon,
SupportAgentIcon,
} from "./ui/icons";
import tinycolor from "tinycolor2";

/**
Expand Down Expand Up @@ -378,6 +390,7 @@ button {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
z-index: 20;
gap: 20px;
}
/** Only add spacing margins from the third child onwards (as the first child is the dialog container) */
Expand Down Expand Up @@ -453,22 +466,24 @@ button {
text-align: center;
}
.drawer-footer button {
/** Discrete button */
.discrete-button {
display: inline-flex;
align-items: center;
border: none;
background: none;
color: #777;
}
.drawer-footer button svg {
.discrete-button svg {
flex: 0 0 14px;
height: 14px;
display: inline-block;
margin-right: 4px;
}
.drawer-footer button:hover {
.discrete-button:hover {
color: #000;
}
Expand All @@ -495,6 +510,21 @@ button {
color: red;
text-align: center;
}
/** Confirmation */
.confirmation {
display: flex;
flex-direction: column;
gap: 10px;
}
.confirmation-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
/** Highlights */
Expand All @@ -515,49 +545,7 @@ button {
`;
};

const MultimodalIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C11.4477 20 11 20.4477 11 21C11 21.5523 11.4477 22 12 22C16.456 22 20.2313 19.0855 21.5236 15.0587C22.3766 14.8106 23 14.0231 23 13.09V11.09C23 10.1795 22.4064 9.40764 21.5851 9.14028C20.3549 5.01091 16.5291 2 12 2C7.47091 2 3.64506 5.01091 2.41488 9.14028C1.59358 9.40764 1 10.1795 1 11.09V13.09C1 14.1666 1.82988 15.0493 2.88483 15.1334C2.92262 15.1378 2.96105 15.14 3 15.14C3.55228 15.14 4 14.6923 4 14.14V12Z"></path>
<path d="M10.09 10.8L15.2453 6.27083L13.9005 13L8.74526 17.5292L10.09 10.8ZM12.8613 12.4C12.5851 12.8783 11.9735 13.0422 11.4953 12.766C11.017 12.4899 10.8531 11.8783 11.1292 11.4C11.4054 10.9217 12.017 10.7578 12.4953 11.034C12.9735 11.3101 13.1374 11.9217 12.8613 12.4Z"></path>
</svg>
);

const SupportAgentIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M21 12.22C21 6.73 16.74 3 12 3c-4.69 0-9 3.65-9 9.28-.6.34-1 .98-1 1.72v2c0 1.1.9 2 2 2h1v-6.1c0-3.87 3.13-7 7-7s7 3.13 7 7V19h-8v2h8c1.1 0 2-.9 2-2v-1.22c.59-.31 1-.92 1-1.64v-2.3c0-.7-.41-1.31-1-1.62"></path>
<circle cx="9" cy="13" r="1"></circle>
<circle cx="15" cy="13" r="1"></circle>
<path d="M18 11.03C17.52 8.18 15.04 6 12.05 6c-3.03 0-6.29 2.51-6.03 6.45 2.47-1.01 4.33-3.21 4.86-5.89 1.31 2.63 4 4.44 7.12 4.47"></path>
</svg>
);

const ArrowBackIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"></path>
</svg>
);

const CallEndIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9"></path>
</svg>
);

const CloseIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</svg>
);

const CheckIcon: FunctionComponent<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path>
</svg>
);

const Highlight: FunctionComponent<{ element: HTMLElement }> = ({
element,
}) => {
const Highlight: FC<{ element: HTMLElement }> = ({ element }) => {
const ref = useRef<HTMLDivElement | null>(null);
const [rect, setRect] = useState<{
x: number;
Expand Down Expand Up @@ -624,9 +612,7 @@ type ControlCenterStatus =
| "success-escalation"
| "success-end";

const SuccessMessage: FunctionComponent<{ message: string }> = ({
message,
}) => {
const SuccessMessage: FC<{ message: string }> = ({ message }) => {
return (
<p className="success-message">
<span>
Expand All @@ -637,15 +623,42 @@ const SuccessMessage: FunctionComponent<{ message: string }> = ({
);
};

const DrawerDialog: FunctionComponent<{ children: VNode<any> }> = ({
children,
}) => {
const CloseButton: FC<{ onClose: () => void }> = ({ onClose }) => {
return (
<button className="discrete-button" onClick={onClose}>
<CloseIcon />
<span>Close</span>
</button>
);
};

const DrawerDialog: FC<{ children: ComponentChildren }> = ({ children }) => {
return <div className="drawer-dialog">{children}</div>;
};

const EscalationButton: FunctionComponent<{
const Confirmation: FC<{
content: string;
onConfirm: () => void;
pending?: boolean;
onCancel: () => void;
}> = ({ content, onConfirm, onCancel, pending }) => {
return (
<div className="confirmation">
<p>{content}</p>
<div className="confirmation-buttons">
<button disabled={pending} onClick={onConfirm}>
Confirm
</button>
<button onClick={onCancel}>Cancel</button>
</div>
</div>
);
};

const EscalationButton: FC<{
onEscalation: (config: SimpleHandlerArg) => void;
escalationButtonLabel?: string;
escalationConfirmation?: string;
client: Client;
onClose: () => void;
drawerDialogRef: MutableRef<HTMLDivElement | null>;
Expand All @@ -655,47 +668,67 @@ const EscalationButton: FunctionComponent<{
onClose,
client,
escalationButtonLabel,
escalationConfirmation,
}) => {
const [status, setStatus] = useState<
"confirming" | "pending" | "success" | null
>(null);

const onSubmit = () => {
setStatus("pending");
onEscalation?.({ sendStep: client.sendStep });
setTimeout(() => {
setStatus("success");
}, 800);
setTimeout(() => {
onClose();
}, 5000);
};

return (
<>
{status === "confirming"
{status === "confirming" ||
(status === "pending" && escalationConfirmation != null)
? createPortal(
<DrawerDialog>
<p>abcd</p>
<Confirmation
content={escalationConfirmation ?? ""}
onConfirm={onSubmit}
pending={status === "pending"}
onCancel={() => {
setStatus(null);
}}
/>
</DrawerDialog>,
drawerDialogRef.current!,
)
: null}
{status === "success" ? (
<SuccessMessage message="Your call is being transferred to an agent." />
) : (
<button
disabled={status === "pending"}
onClick={() => {
: status === "success"
? createPortal(
<DrawerDialog>
<SuccessMessage message="Your call has been transferred to an agent." />
<CloseButton onClose={onClose} />
</DrawerDialog>,
drawerDialogRef.current!,
)
: null}
<button
disabled={status === "pending"}
onClick={() => {
if (escalationConfirmation != null) {
setStatus("confirming");
return;
onEscalation?.({ sendStep: client.sendStep });
setStatus("pending");
setTimeout(() => {
setStatus("success");
}, 800);
setTimeout(() => {
onClose();
}, 5000);
}}
>
<SupportAgentIcon />
{escalationButtonLabel ?? "Escalate to Agent"}
</button>
)}
}
onSubmit();
}}
>
<SupportAgentIcon />
{escalationButtonLabel ?? "Escalate to Agent"}
</button>
</>
);
};

const ControlCenter: FunctionComponent<{
const ControlCenter: FC<{
config: UiConfig;
client: Client;
triggeredSteps: TriggeredStep[];
Expand Down Expand Up @@ -803,7 +836,8 @@ const ControlCenter: FunctionComponent<{
drawerContentRef.current != null &&
!drawerContentRef.current.contains(event.target as Node)
) {
setIsOpen(false);
// Does not work due to portal component bubbling
// setIsOpen(false);
}
}}
>
Expand Down Expand Up @@ -837,6 +871,7 @@ const ControlCenter: FunctionComponent<{
onEscalation={config.onEscalation}
client={client}
escalationButtonLabel={config.escalationButtonLabel}
escalationConfirmation={config.escalationConfirmation}
onClose={() => {
setIsOpen(false);
}}
Expand Down Expand Up @@ -878,14 +913,11 @@ const ControlCenter: FunctionComponent<{
</div>
)}
<div className="drawer-footer">
<button
onClick={() => {
<CloseButton
onClose={() => {
setIsOpen(false);
}}
>
<CloseIcon />
<span>Close</span>
</button>
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -998,13 +1030,7 @@ interface PinBubbleProps {
onClick: () => void;
}

// eslint-disable-next-line jsdoc/require-returns, jsdoc/require-param
/** @hidden @internal */
export const PinBubble: FunctionComponent<PinBubbleProps> = ({
isActive,
content,
onClick,
}) => (
const PinBubble: FC<PinBubbleProps> = ({ isActive, content, onClick }) => (
<div className={`pin-bubble-container ${isActive ? "active" : "inactive"}`}>
<div className="pin-bubble-content">{content}</div>
<button className="pin-bubble-button" onClick={onClick}>
Expand Down
Empty file.
41 changes: 41 additions & 0 deletions packages/journey-manager/src/ui/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type FunctionComponent as FC } from "preact";

export const MultimodalIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C11.4477 20 11 20.4477 11 21C11 21.5523 11.4477 22 12 22C16.456 22 20.2313 19.0855 21.5236 15.0587C22.3766 14.8106 23 14.0231 23 13.09V11.09C23 10.1795 22.4064 9.40764 21.5851 9.14028C20.3549 5.01091 16.5291 2 12 2C7.47091 2 3.64506 5.01091 2.41488 9.14028C1.59358 9.40764 1 10.1795 1 11.09V13.09C1 14.1666 1.82988 15.0493 2.88483 15.1334C2.92262 15.1378 2.96105 15.14 3 15.14C3.55228 15.14 4 14.6923 4 14.14V12Z"></path>
<path d="M10.09 10.8L15.2453 6.27083L13.9005 13L8.74526 17.5292L10.09 10.8ZM12.8613 12.4C12.5851 12.8783 11.9735 13.0422 11.4953 12.766C11.017 12.4899 10.8531 11.8783 11.1292 11.4C11.4054 10.9217 12.017 10.7578 12.4953 11.034C12.9735 11.3101 13.1374 11.9217 12.8613 12.4Z"></path>
</svg>
);

export const SupportAgentIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M21 12.22C21 6.73 16.74 3 12 3c-4.69 0-9 3.65-9 9.28-.6.34-1 .98-1 1.72v2c0 1.1.9 2 2 2h1v-6.1c0-3.87 3.13-7 7-7s7 3.13 7 7V19h-8v2h8c1.1 0 2-.9 2-2v-1.22c.59-.31 1-.92 1-1.64v-2.3c0-.7-.41-1.31-1-1.62"></path>
<circle cx="9" cy="13" r="1"></circle>
<circle cx="15" cy="13" r="1"></circle>
<path d="M18 11.03C17.52 8.18 15.04 6 12.05 6c-3.03 0-6.29 2.51-6.03 6.45 2.47-1.01 4.33-3.21 4.86-5.89 1.31 2.63 4 4.44 7.12 4.47"></path>
</svg>
);

export const ArrowBackIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"></path>
</svg>
);

export const CallEndIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9"></path>
</svg>
);

export const CloseIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</svg>
);

export const CheckIcon: FC<unknown> = () => (
<svg viewBox="0 0 24 24" stroke="none" fill="currentColor">
<path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path>
</svg>
);
2 changes: 2 additions & 0 deletions packages/website/src/components/Prototyping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const runJourneyManager = async (): Promise<unknown> => {
colors: { highlight: "#7dd3fc" },
},
escalationButtonLabel: "Hand Off To Specialist",
escalationConfirmation:
"You are about to hand off to a specialist. Are you sure?",
onEscalation: (args) => {
// eslint-disable-next-line no-console
console.log("escalation", args);
Expand Down

0 comments on commit 9cf1af5

Please sign in to comment.