diff --git a/packages/chat-core/src/index.ts b/packages/chat-core/src/index.ts index 2589e043..5b5392e9 100644 --- a/packages/chat-core/src/index.ts +++ b/packages/chat-core/src/index.ts @@ -75,7 +75,17 @@ export type UserResponsePayload = context?: Context; } & StructuredRequest); -export type Response = BotResponse | UserResponse; +// Failure message + +export interface FailureMessage { + type: "failure"; + payload: { + text: string; + }; + receivedAt: Time; +} + +export type Response = BotResponse | UserResponse | FailureMessage; export type Time = number; @@ -88,8 +98,7 @@ export interface Config { conversationId?: string; userId?: string; responses?: Response[]; - failureMessages?: string[]; - greetingMessages?: string[]; + failureMessage?: string; environment?: Environment; headers?: { [key: string]: string; @@ -106,9 +115,7 @@ export interface Config { const welcomeIntent = "NLX.Welcome"; -const defaultFailureMessages = [ - "We encountered an issue. Please try again soon.", -]; +const defaultFailureMessage = "We encountered an issue. Please try again soon."; export type State = Response[]; @@ -214,27 +221,7 @@ export const createConversation = (config: Config): ConversationHandler => { const initialConversationId = config.conversationId || uuid(); let state: InternalState = { - responses: - config.responses || - (config.greetingMessages && config.greetingMessages.length > 0 - ? [ - { - type: "bot", - receivedAt: new Date().getTime(), - payload: { - conversationId: initialConversationId, - messages: config.greetingMessages.map( - (greetingMessage: string) => ({ - messageId: undefined, - text: greetingMessage, - choices: [] as Array, - selectedChoiceId: undefined, - }), - ), - }, - }, - ] - : []), + responses: config.responses || [], userId: config.userId, conversationId: initialConversationId, }; @@ -255,15 +242,10 @@ export const createConversation = (config: Config): ConversationHandler => { const failureHandler = () => { const newResponse: Response = { - type: "bot", + type: "failure", receivedAt: new Date().getTime(), payload: { - messages: (config.failureMessages || defaultFailureMessages).map( - (messageBody: string): BotMessage => ({ - text: messageBody, - choices: [] as Array, - }), - ), + text: config.failureMessage || defaultFailureMessage, }, }; setState( diff --git a/packages/chat-widget/src/icons.tsx b/packages/chat-widget/src/icons.tsx index 61dd5d63..da80081a 100644 --- a/packages/chat-widget/src/icons.tsx +++ b/packages/chat-widget/src/icons.tsx @@ -23,3 +23,9 @@ export const DownloadIcon = () => ( ); + +export const ErrorOutlineIcon = () => ( + + + +); diff --git a/packages/chat-widget/src/index.tsx b/packages/chat-widget/src/index.tsx index d711700d..622424b9 100644 --- a/packages/chat-widget/src/index.tsx +++ b/packages/chat-widget/src/index.tsx @@ -17,7 +17,7 @@ import { createRoot } from "react-dom/client"; import { ThemeProvider } from "@emotion/react"; import { useChat, type ChatHook } from "@nlxai/chat-react"; import { type Response, type ConversationHandler } from "@nlxai/chat-core"; -import { CloseIcon, ChatIcon, AirplaneIcon } from "./icons"; +import { CloseIcon, ChatIcon, AirplaneIcon, ErrorOutlineIcon } from "./icons"; import * as constants from "./ui/constants"; import { type Props, @@ -110,70 +110,86 @@ const MessageGroups: FC<{ customModalities: Record; }> = (props) => ( - {props.chat.responses.map((response, responseIndex) => - response.type === "bot" ? ( - - {response.payload.messages.map((botMessage, botMessageIndex) => ( - + {props.chat.responses.map((response, responseIndex) => { + if (response.type === "bot") { + return ( + + {response.payload.messages.map((botMessage, botMessageIndex) => ( + + + {botMessage.choices.length > 0 && ( + + {botMessage.choices.map((choice, choiceIndex) => ( + { + return botMessage.selectedChoiceId + ? { + disabled: true, + selected: + botMessage.selectedChoiceId === + choice.choiceId, + } + : { + onClick: () => { + props.chat.conversationHandler.sendChoice( + choice.choiceId, + ); + }, + }; + })()} + dangerouslySetInnerHTML={{ + __html: marked( + choice.choiceText + + (false ? " asdf fadsfds fdsa fdsa fdsa " : ""), + ), + }} + > + ))} + + )} + + ))} + {Object.entries(response.payload.modalities || {}).map( + ([key, value]) => { + const Component = props.customModalities[key]; + if (Component) { + return ; + } + return null; + }, + )} + + ); + } + + if (response.type === "failure") { + return ( + + + {response.payload.text} + + ); + } + + if (response.type === "user" && response.payload.type === "text") { + return ( + + - {botMessage.choices.length > 0 && ( - - {botMessage.choices.map((choice, choiceIndex) => ( - { - return botMessage.selectedChoiceId - ? { - disabled: true, - selected: - botMessage.selectedChoiceId === choice.choiceId, - } - : { - onClick: () => { - props.chat.conversationHandler.sendChoice( - choice.choiceId, - ); - }, - }; - })()} - dangerouslySetInnerHTML={{ - __html: marked( - choice.choiceText + - (false ? " asdf fadsfds fdsa fdsa fdsa " : ""), - ), - }} - > - ))} - - )} - ))} - {Object.entries(response.payload.modalities || {}).map( - ([key, value]) => { - const Component = props.customModalities[key]; - if (Component) { - return ; - } - return null; - }, - )} - - ) : response.payload.type === "text" ? ( - - - - - - ) : null, - )} + + ); + } + })} {props.children} ); diff --git a/packages/chat-widget/src/ui/components.tsx b/packages/chat-widget/src/ui/components.tsx index 13bc9a09..180a6438 100644 --- a/packages/chat-widget/src/ui/components.tsx +++ b/packages/chat-widget/src/ui/components.tsx @@ -53,7 +53,7 @@ const Dot = styled.div<{}>` height: 6px; flex: 0 0 6px; border-radius: 50%; - background-color: ${props => props.theme.darkMessageColor}; + background-color: ${(props) => props.theme.darkMessageColor}; animation: ${bounceKeyframes} 1s infinite ease-in-out; margin-right: 4px; &:last-of-type { @@ -102,21 +102,21 @@ const bottom = 90; export const Container = styled.div<{}>` position: fixed; - top: ${props => { + top: ${(props) => { if (!props.theme.windowInnerHeight) { return `${top}px`; } return `${Math.max( props.theme.windowInnerHeight - props.theme.chatWindowMaxHeight - bottom, - top + top, )}px`; }}; right: ${top}px; bottom: ${bottom}px; width: calc(100% - 40px); - border-radius: ${props => props.theme.borderRadius}px; + border-radius: ${(props) => props.theme.borderRadius}px; box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.3); - background-color: ${props => props.theme.white}; + background-color: ${(props) => props.theme.white}; z-index: ${constants.largeZIndex}; @media screen and (min-width: 360px) { @@ -126,7 +126,7 @@ export const Container = styled.div<{}>` } & * { - font-family: ${props => props.theme.fontFamily}; + font-family: ${(props) => props.theme.fontFamily}; } `; @@ -140,14 +140,14 @@ export const Main = styled.div<{}>` // MessageGroups export const MessageGroups = styled.div<{}>` - padding: ${props => props.theme.spacing}px; + padding: ${(props) => props.theme.spacing}px; box-sizing: border-box; z-index: 1; position: relative; & > * { - margin-bottom: ${props => props.theme.spacing}px; + margin-bottom: ${(props) => props.theme.spacing}px; } & > :last-child { @@ -162,7 +162,7 @@ export const MessageGroup = styled.div<{}>` flex-direction: column; & > * { - margin-bottom: ${props => props.theme.spacing / 2}px; + margin-bottom: ${(props) => props.theme.spacing / 2}px; } & > :last-child { @@ -173,19 +173,39 @@ export const MessageGroup = styled.div<{}>` // Message export const Message = styled.div<{ type: "user" | "bot" }>` - background-color: ${props => + background-color: ${(props) => props.type === "user" ? props.theme.darkMessageColor : props.theme.lightMessageColor}; - color: ${props => (props.type === "user" ? props.theme.white : "#676767")}; - padding: ${props => `${props.theme.spacing}px ${props.theme.spacing}px`}; + color: ${(props) => (props.type === "user" ? props.theme.white : "#676767")}; + padding: ${(props) => `${props.theme.spacing}px ${props.theme.spacing}px`}; max-width: calc(100% - 20px); - ${props => + ${(props) => props.type === "user" ? "margin-left: 20px; margin-right: 0; border-radius: 10px 10px 0 10px; align-self: flex-end;" : "margin-right: 20px; margin-left: 0; border-radius: 10px 10px 10px 0; align-self: flex-start;"} p { - color: ${props => (props.type === "user" ? props.theme.white : "#232323")}; + color: ${(props) => + props.type === "user" ? props.theme.white : "#232323"}; + } +`; + +export const FailureMessage = styled.p<{}>` + display: flex; + align-items: center; + justify-content: flex-start; + font-size: ${constants.smallFontSize}px; + border-radius: ${(props) => props.theme.borderRadius}px; + background-color: ${(props) => props.theme.lightMessageColor}; + color: #454545; + padding: ${(props) => + `${props.theme.spacing / 2}px ${props.theme.spacing}px`}; + & svg { + display: inline-block; + flex: 0 0 ${constants.fontSize}px; + width: ${constants.fontSize}px; + height: ${constants.fontSize}px; + margin-right: 4px; } `; @@ -215,8 +235,8 @@ export const Bottom = styled.div<{}>` height: ${constants.bottomHeight}px; position: relative; border-top: 1px solid rgba(0, 0, 0, 0.08); - border-bottom-left-radius: ${props => props.theme.borderRadius}px; - border-bottom-right-radius: ${props => props.theme.borderRadius}px; + border-bottom-left-radius: ${(props) => props.theme.borderRadius}px; + border-bottom-right-radius: ${(props) => props.theme.borderRadius}px; `; export const IconButton = styled.button<{ disabled?: boolean }>` @@ -226,7 +246,7 @@ export const IconButton = styled.button<{ disabled?: boolean }>` flex: none; padding: 8px; font-size: ${constants.fontSize}px; - ${props => + ${(props) => props.disabled ? ` opacity: 0.6; @@ -235,14 +255,14 @@ export const IconButton = styled.button<{ disabled?: boolean }>` `} border: 0; box-shadow: none; - color: ${props => props.theme.primaryColor}; + color: ${(props) => props.theme.primaryColor}; background: none; position: relative; cursor: pointer; :focus { outline: none; - ${props => focusShadow(props.theme)} + ${(props) => focusShadow(props.theme)} } :disabled { @@ -250,7 +270,7 @@ export const IconButton = styled.button<{ disabled?: boolean }>` } svg { - fill: ${props => props.theme.primaryColor}; + fill: ${(props) => props.theme.primaryColor}; } ${hoverBg} @@ -259,7 +279,7 @@ export const IconButton = styled.button<{ disabled?: boolean }>` export const BottomButtonsContainer = styled.div<{}>` position: absolute; top: 50%; - right: ${props => `${props.theme.spacing}px`}; + right: ${(props) => `${props.theme.spacing}px`}; transform: translate3d(0, -50%, 0); `; @@ -269,7 +289,8 @@ export const Input = styled.input<{}>` width: 100%; height: 100%; background-color: transparent; - padding: ${props => `0 ${2 * props.theme.spacing}px`}; + padding: ${(props) => + `0 ${2 * props.theme.spacing}px 0 ${2 * props.theme.spacing}px`}; font-size: ${constants.fontSize}px; border: none; @@ -280,7 +301,7 @@ export const Input = styled.input<{}>` export const Pin = styled.button<{}>` position: fixed; - background-color: ${props => props.theme.primaryColor}; + background-color: ${(props) => props.theme.primaryColor}; border: 0; right: 20px; bottom: 20px; @@ -289,7 +310,7 @@ export const Pin = styled.button<{}>` border-radius: 30px; cursor: pointer; padding: 12px; - color: ${props => props.theme.white}; + color: ${(props) => props.theme.white}; box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.4); z-index: ${constants.largeZIndex}; @@ -303,7 +324,7 @@ export const Pin = styled.button<{}>` } > svg { - fill: ${props => props.theme.white}; + fill: ${(props) => props.theme.white}; } ${hoverBg} @@ -315,7 +336,7 @@ export const PinBubble: React.FunctionComponent<{ isActive: boolean; content: string; onClick: () => void; -}> = props => ( +}> = (props) => ( @@ -345,25 +366,25 @@ export const PinBubbleContainer = styled.div<{ line-height: 1; z-index: ${constants.largeZIndex}; padding: 0px 16px 0px 0px; - background-color: ${props => - tinycolor(props.theme.primaryColor) - .darken(10) - .toRgbString()}; + background-color: ${(props) => + tinycolor(props.theme.primaryColor).darken(10).toRgbString()}; color: #fff; - transition: opacity 0.2s, transform 0.2s; - ${props => - props.isActive - ? ` + transition: + opacity 0.2s, + transform 0.2s; + ${(props) => + props.isActive + ? ` opacity: 1; transform: translate3d(0, 0, 0); pointer-events: all; ` - : ` + : ` opacity: 0; transform: translate3d(0, 10px, 0); pointer-events: none; `} - ::after { + ::after { position: absolute; top: 32px; right: 22px; @@ -373,10 +394,7 @@ export const PinBubbleContainer = styled.div<{ border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid - ${props => - tinycolor(props.theme.primaryColor) - .darken(10) - .toRgbString()}; + ${(props) => tinycolor(props.theme.primaryColor).darken(10).toRgbString()}; } `; @@ -386,7 +404,7 @@ export const PinBubbleButton = styled.button<{}>` width: 32px; height: 32px; border: 0; - color: ${props => props.theme.white}; + color: ${(props) => props.theme.white}; cursor: pointer; padding: 6px; display: flex; @@ -400,7 +418,7 @@ export const PinBubbleButton = styled.button<{}>` svg { width: 100%; height: 100%; - fill: ${props => props.theme.white}; + fill: ${(props) => props.theme.white}; } :hover { background-color: rgba(255, 255, 255, 0.1); @@ -433,12 +451,12 @@ export const ChoiceButton = styled.button<{ disabled?: boolean; selected?: boolean; }>` - ${props => + ${(props) => ` background-color: ${props.theme.primaryColor}; color: ${props.theme.white}; `} - ${props => + ${(props) => props.selected ? ` outline: 2px solid ${props.theme.primaryColor}; @@ -446,7 +464,7 @@ export const ChoiceButton = styled.button<{ ` : ` `} - ${props => { + ${(props) => { const hoverColor = tinycolor(props.theme.primaryColor) .brighten(5) .toRgbString(); @@ -468,8 +486,8 @@ export const ChoiceButton = styled.button<{ } `; }} - border-radius: ${props => props.theme.borderRadius}px; - border: 1px solid ${props => props.theme.primaryColor}; + border-radius: ${(props) => props.theme.borderRadius}px; + border: 1px solid ${(props) => props.theme.primaryColor}; font-size: ${constants.fontSize}px; padding: 4px 12px; @@ -483,7 +501,7 @@ export const ChoiceButton = styled.button<{ font-size: ${constants.fontSize}px; margin: 0; - ${props => ` + ${(props) => ` color: ${props.theme.white}; `} } @@ -499,11 +517,11 @@ export const ChoiceButton = styled.button<{ export const TitleBar = styled.div<{}>` height: ${constants.bottomHeight}px; - padding: 0 ${props => 2 * props.theme.spacing}px; - border-top-left-radius: ${props => props.theme.borderRadius}px; - border-top-right-radius: ${props => props.theme.borderRadius}px; - background-color: ${props => props.theme.primaryColor}; - color: ${props => props.theme.white}; + padding: 0 ${(props) => 2 * props.theme.spacing}px; + border-top-left-radius: ${(props) => props.theme.borderRadius}px; + border-top-right-radius: ${(props) => props.theme.borderRadius}px; + background-color: ${(props) => props.theme.primaryColor}; + color: ${(props) => props.theme.white}; display: flex; align-items: center; justify-content: space-between; @@ -526,13 +544,13 @@ export const Title = styled.p<{}>` font-size: 16px; font-weight: bold; margin: 0; - color: ${props => props.theme.white}; + color: ${(props) => props.theme.white}; `; // DiscreteButton export const DiscreteLink = styled.a<{}>` - color: ${props => props.theme.white}; + color: ${(props) => props.theme.white}; border: 0; display: inline-flex; width: 12px; @@ -551,7 +569,7 @@ export const DiscreteLink = styled.a<{}>` box-shadow: 0 0 0 3px #dedede; } > svg { - color: ${props => props.theme.white}; + color: ${(props) => props.theme.white}; width: 12px; height: 12px; } diff --git a/packages/chat-widget/src/ui/constants.ts b/packages/chat-widget/src/ui/constants.ts index 63d02a4d..f6b8ba42 100644 --- a/packages/chat-widget/src/ui/constants.ts +++ b/packages/chat-widget/src/ui/constants.ts @@ -4,6 +4,8 @@ export const bottomHeight = 60; export const fontSize = 15; +export const smallFontSize = 12; + export const largeZIndex = 2147483000; export const defaultTheme: Theme = { diff --git a/packages/website/package.json b/packages/website/package.json index 8de96a08..9ec77f71 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.1", "scripts": { - "dev": "vite", + "dev": "vite --force", "build": "tsc && vite build" }, "dependencies": { diff --git a/packages/website/src/components/ChatConfiguration.tsx b/packages/website/src/components/ChatConfiguration.tsx index 291f8d35..6c093c96 100644 --- a/packages/website/src/components/ChatConfiguration.tsx +++ b/packages/website/src/components/ChatConfiguration.tsx @@ -1,9 +1,5 @@ import React, { type FC, type ReactNode } from "react"; -import { - type Theme, - type TitleBar, - defaultTheme, -} from "@nlxai/chat-widget"; +import { type Theme, type TitleBar, defaultTheme } from "@nlxai/chat-widget"; import { type Config } from "@nlxai/chat-core"; import { Behavior } from "../snippets"; @@ -17,6 +13,7 @@ export const getInitialConfig = (): Config => { headers: { "nlx-api-key": apiKey, }, + failureMessage: "Something went wrong", languageCode, }; }; @@ -33,7 +30,8 @@ const Labeled: FC<{ label: string; children: ReactNode }> = ({ ); }; -const inputClass = "text-sm px-1.5 py-1 block w-full rounded border border-gray-300"; +const inputClass = + "text-sm px-1.5 py-1 block w-full rounded border border-gray-300"; export const TitleBarEditor: FC<{ value: TitleBar; @@ -135,7 +133,7 @@ export const ThemeEditor: FC<{ - ) + ), )}