Skip to content

Commit

Permalink
Merge branch 'add-toast'
Browse files Browse the repository at this point in the history
  • Loading branch information
tjtanjin committed Sep 4, 2024
2 parents 03fb051 + 85fb16b commit 3c3e5a4
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import "./FileAttachmentButton.css";
* @param openChat utility function to open/close chat window
* @param getCurrPath retrieves current path for the user
* @param getPrevPath retrieves previous path for the user
* @param goToPath goes to specified path
* @param setTextAreaValue sets the value within the text area
* @param injectToast injects a toast message prompt
* @param handleActionInput handles action input from user
*/
const FileAttachmentButton = ({
Expand All @@ -31,6 +34,7 @@ const FileAttachmentButton = ({
getPrevPath,
goToPath,
setTextAreaValue,
injectToast,
handleActionInput
}: {
inputRef: RefObject<HTMLTextAreaElement | HTMLInputElement>;
Expand All @@ -43,6 +47,7 @@ const FileAttachmentButton = ({
getPrevPath: () => keyof Flow | null;
goToPath: (pathToGo: keyof Flow) => void;
setTextAreaValue: (value: string) => void;
injectToast: (content: string | JSX.Element, timeout?: number) => void;
handleActionInput: (path: keyof Flow, userInput: string, sendUserInput?: boolean) => Promise<void>;
}) => {

Expand Down Expand Up @@ -112,7 +117,7 @@ const FileAttachmentButton = ({
}
await handleActionInput(currPath, "📄 " + fileNames.join(", "), settings.fileAttachment?.sendFileName);
await fileHandler({userInput: inputRef.current?.value as string, prevPath: getPrevPath(),
goToPath, setTextAreaValue, injectMessage, streamMessage, openChat, files});
goToPath, setTextAreaValue, injectMessage, streamMessage, openChat, injectToast, files});
}
};

Expand Down
18 changes: 17 additions & 1 deletion src/components/ChatBotBody/ChatBotBody.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,20 @@
100% {
opacity: 0.4;
}
}
}

/* Toast Container */
.rcb-toast-prompt-container {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 200px;
margin: auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
opacity: 1;
animation: popIn 0.3s ease-in-out;
pointer-events: auto;
}
30 changes: 30 additions & 0 deletions src/components/ChatBotBody/ChatBotBody.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { RefObject, Dispatch, SetStateAction, useEffect, CSSProperties, MouseEvent } from "react";

import ChatMessagePrompt from "./ChatMessagePrompt/ChatMessagePrompt";
import ToastPrompt from "./ToastPrompt/ToastPrompt";
import { useSettings } from "../../context/SettingsContext";
import { useStyles } from "../../context/StylesContext";
import { useMessages } from "../../context/MessagesContext";
import { Message } from "../../types/Message";
import { Toast } from "../../types/internal/Toast";

import "./ChatBotBody.css";

Expand All @@ -21,6 +23,8 @@ import "./ChatBotBody.css";
* @param setIsScrolling setter for tracking if user is scrolling
* @param unreadCount number representing unread messages count
* @param setUnreadCount setter for unread messages count
* @param toasts toasts to be shown
* @param removeToast removes a toast by id
*/
const ChatBotBody = ({
chatBodyRef,
Expand All @@ -33,6 +37,8 @@ const ChatBotBody = ({
setIsScrolling,
unreadCount,
setUnreadCount,
toasts,
removeToast
}: {
chatBodyRef: RefObject<HTMLDivElement>;
isBotTyping: boolean;
Expand All @@ -44,6 +50,8 @@ const ChatBotBody = ({
setIsScrolling: Dispatch<SetStateAction<boolean>>;
unreadCount: number;
setUnreadCount: Dispatch<SetStateAction<number>>;
toasts: Array<Toast>,
removeToast: (id: string) => void;
}) => {

// handles settings for bot
Expand Down Expand Up @@ -79,6 +87,16 @@ const ChatBotBody = ({
};
const botBubbleEntryStyle = settings.botBubble?.animate ? "rcb-bot-message-entry" : "";

// styles for toast prompt container
const toastPromptContainerStyle: CSSProperties = {
bottom: (styles.chatInputContainerStyle?.height as number || 70) +
(styles.footerStyle?.height as number || 50) + 15,
width: 300,
minWidth: (styles.chatWindowStyle?.width as number || 375) / 2,
maxWidth: (styles.chatWindowStyle?.width as number || 375) - 50,
...styles.toastPromptContainerStyle
};

// shifts scroll position when messages are updated and when bot is typing
useEffect(() => {
if (!chatBodyRef.current) {
Expand Down Expand Up @@ -284,6 +302,18 @@ const ChatBotBody = ({
chatBodyRef={chatBodyRef} isScrolling={isScrolling}
setIsScrolling={setIsScrolling} unreadCount={unreadCount}
/>

<div className="rcb-toast-prompt-container" style={toastPromptContainerStyle}>
{toasts.map((toast) => (
<ToastPrompt
key={toast.id}
id={toast.id}
content={toast.content}
removeToast={removeToast} // Function to remove toast
timeout={toast.timeout} // Timeout for auto-removal
/>
))}
</div>
</div>
);
};
Expand Down
28 changes: 28 additions & 0 deletions src/components/ChatBotBody/ToastPrompt/ToastPrompt.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.rcb-toast-prompt-text {
padding: 6px 12px;
border-radius: 5px;
color: #7a7a7a;
font-size: 12px;
text-align: center;
background-color: #fff;
border: 0.5px solid #7a7a7a;
cursor: pointer;
transition: color 0.3s ease, border-color 0.3s ease;
z-index: 9999;
width: 100%;
margin-top: 6px;
}

@keyframes popIn {
0% {
transform: scale(0.8);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
}
}
92 changes: 92 additions & 0 deletions src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect, useState, MouseEvent } from "react";

import { useSettings } from "../../../context/SettingsContext";
import { useStyles } from "../../../context/StylesContext";

import "./ToastPrompt.css";

/**
* Provides toast message prompt with information.
*
* @param id id of the toast
* @param content content of the toast
* @param removeToast removes a toast by id
* @param timeout timeout in milliseconds (optional) for removing toast
*/
const Toast = ({
id,
content,
removeToast,
timeout,
}: {
id: string;
content: string | JSX.Element;
removeToast: (id: string) => void;
timeout?: number;
}) => {

// handles settings for bot
const { settings } = useSettings();

// handles styles for bot
const { styles } = useStyles();

// tracks if toast prompt is hovered
const [isHovered, setIsHovered] = useState<boolean>(false);

// styles for toast prompt hovered
const toastPromptHoveredStyle: React.CSSProperties = {
color: settings.general?.primaryColor,
borderColor: settings.general?.primaryColor,
...styles.toastPromptHoveredStyle
};

useEffect(() => {
// if timeout is set, dismiss toast after specified period
if (timeout) {
const timer = setTimeout(() => {
removeToast(id);
}, timeout);
return () => clearTimeout(timer);
}
}, [id, removeToast, timeout]);

/**
* Handles mouse enter event on toast prompt.
*/
const handleMouseEnter = () => {
setIsHovered(true);
};

/**
* Handles mouse leave event on toast prompt.
*/
const handleMouseLeave = () => {
setIsHovered(false);
};

return (
typeof content === "string" ? (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={isHovered ? toastPromptHoveredStyle : styles.toastPromptStyle}
onMouseDown={(event: MouseEvent) => {
if (settings.toast?.dismissOnClick) {
event.preventDefault();
removeToast(id);
}
}}
className="rcb-toast-prompt-text"
>
{content}
</div>
) : (
<>
{content}
</>
)
);
};

export default Toast;
49 changes: 41 additions & 8 deletions src/components/ChatBotContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Block } from "../types/Block";
import { Flow } from "../types/Flow";
import { Message } from "../types/Message";
import { Params } from "../types/Params";
import { Toast } from "../types/internal/Toast";
import { Button } from "../constants/Button";

import "./ChatBotContainer.css";
Expand Down Expand Up @@ -111,6 +112,9 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
// tracks typing state of chat bot
const [isBotTyping, setIsBotTyping] = useState<boolean>(false);

// tracks toasts shown
const [toasts, setToasts] = useState<Array<Toast>>([]);

// tracks block timeout if transition is interruptable
const [timeoutId, setTimeoutId] = useState<ReturnType<typeof setTimeout>>();

Expand Down Expand Up @@ -282,7 +286,7 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
}

const params = {prevPath: getPrevPath(), goToPath, setTextAreaValue, userInput: paramsInputRef.current,
injectMessage, streamMessage, openChat};
injectMessage, streamMessage, openChat, injectToast};

// calls the new block for preprocessing upon change to path.
const callNewBlock = async (currPath: keyof Flow, block: Block, params: Params) => {
Expand Down Expand Up @@ -439,6 +443,35 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
}
}

/**
* Injects a new toast.
*
* @param content message to show in toast
* @param timeout optional timeout in milliseconds before toast is removed
*/
const injectToast = useCallback((content: string | JSX.Element, timeout?: number): void => {
setToasts((prevToasts: Toast[]) => {
if (prevToasts.length >= (settings.toast?.maxCount || 3)) {
// if toast array is full and forbidden to add new ones, return existing toasts
if (settings.toast?.forbidOnMax) {
return prevToasts;
}
// else remove the oldest toast
return [...prevToasts.slice(1), { id: crypto.randomUUID(), content, timeout }];
}
return [...prevToasts, { id: crypto.randomUUID(), content, timeout }];
});
}, [setToasts]);

/**
* Removes a toast.
*
* @param id id of toast to remove
*/
const removeToast = useCallback((id: string): void => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
}, [setToasts]);

/**
* Injects a message at the end of the messages array.
*
Expand Down Expand Up @@ -675,7 +708,7 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {

setTimeout(async () => {
const params = {prevPath: getPrevPath(), goToPath, setTextAreaValue, userInput,
injectMessage, streamMessage,openChat
injectMessage, streamMessage, openChat, injectToast
};
const hasNextPath = await postProcessBlock(flow, path, params, setPaths);
if (!hasNextPath) {
Expand All @@ -698,7 +731,7 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
}
}, settings.chatInput?.botDelay);
}, [timeoutId, voiceToggledOn, settings, flow, getPrevPath, injectMessage, streamMessage, openChat,
postProcessBlock, setPaths, handleSendUserInput
postProcessBlock, setPaths, handleSendUserInput, injectToast
]);

/**
Expand Down Expand Up @@ -777,11 +810,11 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {

const fileAttachmentButtonComponentMap = useMemo(() => ({
[Button.FILE_ATTACHMENT_BUTTON]: () => createFileAttachmentButton(inputRef, flow, blockAllowsAttachment,
injectMessage, streamMessage, openChat, getCurrPath, getPrevPath, goToPath, setTextAreaValue,
handleActionInput
injectMessage, streamMessage, openChat, getCurrPath, getPrevPath, goToPath, setTextAreaValue, injectToast,
handleActionInput,
)
}), [inputRef, flow, blockAllowsAttachment, injectMessage, streamMessage, openChat,
getCurrPath, getPrevPath, goToPath, handleActionInput
getCurrPath, getPrevPath, goToPath, handleActionInput, injectToast
]);

const buttonComponentMap = useMemo(() => ({
Expand Down Expand Up @@ -857,8 +890,8 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
<ChatBotBody chatBodyRef={chatBodyRef} isBotTyping={isBotTyping}
isLoadingChatHistory={isLoadingChatHistory} chatScrollHeight={chatScrollHeight}
setChatScrollHeight={setChatScrollHeight} setIsLoadingChatHistory={setIsLoadingChatHistory}
isScrolling={isScrolling} setIsScrolling={setIsScrolling}
unreadCount={unreadCount} setUnreadCount={setUnreadCount}
isScrolling={isScrolling} setIsScrolling={setIsScrolling} unreadCount={unreadCount}
setUnreadCount={setUnreadCount} toasts={toasts} removeToast={removeToast}
/>
{settings.general?.showInputRow &&
<ChatBotInput
Expand Down
5 changes: 5 additions & 0 deletions src/constants/internal/DefaultSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ export const DefaultSettings: Settings = {
icon: emojiIcon,
list: ["😀", "😃", "😄", "😅", "😊", "😌", "😇", "🙃", "🤣", "😍", "🥰", "🥳", "🎉", "🎈", "🚀", "⭐️"]
},
toast: {
maxCount: 3,
forbidOnMax: false,
dismissOnClick: true,
},
advance: {
useAdvancedMessages: false,
useAdvancedSettings: false,
Expand Down
5 changes: 4 additions & 1 deletion src/constants/internal/DefaultStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ export const DefaultStyles: Styles = {
sendIconStyle: {},
rcbTypingIndicatorContainerStyle: {},
rcbTypingIndicatorDotStyle: {},
}
toastPromptContainerStyle: {},
toastPromptStyle: {},
toastPromptHoveredStyle: {}
}
1 change: 1 addition & 0 deletions src/types/Params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Params = {
setTextAreaValue: (value: string) => void;
injectMessage: (content: string | JSX.Element, sender?: string) => Promise<void>;
streamMessage: (content: string | JSX.Element, sender?: string) => Promise<void>;
injectToast: (content: string | JSX.Element, timeout?: number) => void;
openChat: (isOpen: boolean) => void;
files?: FileList;
}
Loading

0 comments on commit 3c3e5a4

Please sign in to comment.