diff --git a/packages/journey-manager/src/ui.tsx b/packages/journey-manager/src/ui.tsx index b3f5e80b..3c0120cc 100644 --- a/packages/journey-manager/src/ui.tsx +++ b/packages/journey-manager/src/ui.tsx @@ -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, @@ -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"; /** @@ -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) */ @@ -453,7 +466,9 @@ button { text-align: center; } -.drawer-footer button { +/** Discrete button */ + +.discrete-button { display: inline-flex; align-items: center; border: none; @@ -461,14 +476,14 @@ button { 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; } @@ -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 */ @@ -515,49 +545,7 @@ button { `; }; -const MultimodalIcon: FunctionComponent = () => ( - - - - -); - -const SupportAgentIcon: FunctionComponent = () => ( - - - - - - -); - -const ArrowBackIcon: FunctionComponent = () => ( - - - -); - -const CallEndIcon: FunctionComponent = () => ( - - - -); - -const CloseIcon: FunctionComponent = () => ( - - - -); - -const CheckIcon: FunctionComponent = () => ( - - - -); - -const Highlight: FunctionComponent<{ element: HTMLElement }> = ({ - element, -}) => { +const Highlight: FC<{ element: HTMLElement }> = ({ element }) => { const ref = useRef(null); const [rect, setRect] = useState<{ x: number; @@ -624,9 +612,7 @@ type ControlCenterStatus = | "success-escalation" | "success-end"; -const SuccessMessage: FunctionComponent<{ message: string }> = ({ - message, -}) => { +const SuccessMessage: FC<{ message: string }> = ({ message }) => { return (

@@ -637,15 +623,42 @@ const SuccessMessage: FunctionComponent<{ message: string }> = ({ ); }; -const DrawerDialog: FunctionComponent<{ children: VNode }> = ({ - children, -}) => { +const CloseButton: FC<{ onClose: () => void }> = ({ onClose }) => { + return ( + + ); +}; + +const DrawerDialog: FC<{ children: ComponentChildren }> = ({ children }) => { return

{children}
; }; -const EscalationButton: FunctionComponent<{ +const Confirmation: FC<{ + content: string; + onConfirm: () => void; + pending?: boolean; + onCancel: () => void; +}> = ({ content, onConfirm, onCancel, pending }) => { + return ( +
+

{content}

+
+ + +
+
+ ); +}; + +const EscalationButton: FC<{ onEscalation: (config: SimpleHandlerArg) => void; escalationButtonLabel?: string; + escalationConfirmation?: string; client: Client; onClose: () => void; drawerDialogRef: MutableRef; @@ -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( -

abcd

+ { + setStatus(null); + }} + />
, drawerDialogRef.current!, ) - : null} - {status === "success" ? ( - - ) : ( - - )} + } + onSubmit(); + }} + > + + {escalationButtonLabel ?? "Escalate to Agent"} + ); }; -const ControlCenter: FunctionComponent<{ +const ControlCenter: FC<{ config: UiConfig; client: Client; triggeredSteps: TriggeredStep[]; @@ -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); } }} > @@ -837,6 +871,7 @@ const ControlCenter: FunctionComponent<{ onEscalation={config.onEscalation} client={client} escalationButtonLabel={config.escalationButtonLabel} + escalationConfirmation={config.escalationConfirmation} onClose={() => { setIsOpen(false); }} @@ -878,14 +913,11 @@ const ControlCenter: FunctionComponent<{ )}
- + />
@@ -998,13 +1030,7 @@ interface PinBubbleProps { onClick: () => void; } -// eslint-disable-next-line jsdoc/require-returns, jsdoc/require-param -/** @hidden @internal */ -export const PinBubble: FunctionComponent = ({ - isActive, - content, - onClick, -}) => ( +const PinBubble: FC = ({ isActive, content, onClick }) => (
{content}