diff --git a/src/components/ChatView/MessageTextarea.tsx b/src/components/ChatView/MessageTextarea.tsx index fdc7827..c82bd8c 100644 --- a/src/components/ChatView/MessageTextarea.tsx +++ b/src/components/ChatView/MessageTextarea.tsx @@ -56,7 +56,7 @@ const MessageTextarea = (props: Props) => { creatorRole: CreatorRole.User, createdAt: Date.now(), content: value, - isGenerated: true, + status: "DONE", }); setValue(""); textareaRef.current!.value = ""; diff --git a/src/components/ChatView/MessageView.tsx b/src/components/ChatView/MessageView.tsx index f99f1f5..a87a0ec 100644 --- a/src/components/ChatView/MessageView.tsx +++ b/src/components/ChatView/MessageView.tsx @@ -1,6 +1,7 @@ import { Menu, MenuItem } from "@mui/material"; import dayjs from "dayjs"; import { ReactElement, useState } from "react"; +import { ThreeDots } from "react-loader-spinner"; import { toast } from "react-hot-toast"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -68,61 +69,71 @@ const MessageView = (props: Props) => { )} -
- - - - ); - }, - code({ children }) { - return `{children}`; - }, - }} - > - {message.content} - - {dayjs(message.createdAt).format("lll")} -
-
- - setMenuAnchorEl(null)} - MenuListProps={{ - "aria-labelledby": "basic-button", - }} - > - - - Copy - - deleteMessage(message)}> - - Delete - - -
+ {message.status === "LOADING" && message.content === "" ? ( +
+ +
+ ) : ( + <> +
+ + + + ); + }, + code({ children }) { + return `{children}`; + }, + }} + > + {message.content} + + {dayjs(message.createdAt).format("lll")} +
+
+ + setMenuAnchorEl(null)} + MenuListProps={{ + "aria-labelledby": "basic-button", + }} + > + + + Copy + + deleteMessage(message)}> + + Delete + + +
+ + )} )} diff --git a/src/components/ChatView/index.tsx b/src/components/ChatView/index.tsx index 11745ff..5fd2360 100644 --- a/src/components/ChatView/index.tsx +++ b/src/components/ChatView/index.tsx @@ -8,7 +8,6 @@ import Header from "./Header"; import EmptyView from "../EmptyView"; import MessageView from "./MessageView"; import MessageTextarea from "./MessageTextarea"; -import MessageLoader from "../MessageLoader"; import DataStorageBanner from "../DataStorageBanner"; // The maximum number of tokens that can be sent to the OpenAI API. @@ -19,17 +18,29 @@ const ChatView = () => { const connectionStore = useConnectionStore(); const chatStore = useChatStore(); const messageStore = useMessageStore(); - const [isRequesting, setIsRequesting] = useState(false); + const [isStickyAtBottom, setIsStickyAtBottom] = useState(true); const [showHeaderShadow, setShowHeaderShadow] = useState(false); const chatViewRef = useRef(null); const currentChat = chatStore.currentChat; const messageList = messageStore.messageList.filter((message) => message.chatId === currentChat?.id); const lastMessage = last(messageList); - // Toggle header shadow. useEffect(() => { + messageStore.messageList.map((message) => { + if (message.status === "LOADING" && message.content === "") { + messageStore.updateMessage(message.id, { + content: "Failed to send the message.", + status: "FAILED", + }); + } + }); + const handleChatViewScroll = () => { + if (!chatViewRef.current) { + return; + } setShowHeaderShadow((chatViewRef.current?.scrollTop || 0) > 0); + setIsStickyAtBottom(chatViewRef.current.scrollTop + chatViewRef.current.clientHeight >= chatViewRef.current.scrollHeight); }; chatViewRef.current?.addEventListener("scroll", handleChatViewScroll); @@ -39,28 +50,21 @@ const ChatView = () => { }, []); useEffect(() => { - setTimeout(() => { - if (!chatViewRef.current) { - return; - } - chatViewRef.current.scrollTop = chatViewRef.current.scrollHeight; - }); - }, [currentChat, messageList.length, lastMessage?.isGenerated]); + if (!chatViewRef.current) { + return; + } + chatViewRef.current.scrollTop = chatViewRef.current.scrollHeight; + }, [currentChat, lastMessage?.id]); useEffect(() => { - setTimeout(() => { - if (!chatViewRef.current) { - return; - } - if (!lastMessage) { - return; - } + if (!chatViewRef.current) { + return; + } - if (!lastMessage.isGenerated) { - chatViewRef.current.scrollTop = chatViewRef.current.scrollHeight; - } - }); - }, [lastMessage?.isGenerated, lastMessage?.content]); + if (lastMessage?.status === "LOADING" && isStickyAtBottom) { + chatViewRef.current.scrollTop = chatViewRef.current.scrollHeight; + } + }, [lastMessage?.status, lastMessage?.content, isStickyAtBottom]); useEffect(() => { if ( @@ -84,14 +88,25 @@ const ChatView = () => { if (!currentChat) { return; } - if (isRequesting) { + if (lastMessage?.status === "LOADING") { return; } - setIsRequesting(true); const messageList = messageStore.getState().messageList.filter((message) => message.chatId === currentChat.id); let prompt = ""; let tokens = 0; + + const message: Message = { + id: generateUUID(), + chatId: currentChat.id, + creatorId: currentChat.assistantId, + creatorRole: CreatorRole.Assistant, + createdAt: Date.now(), + content: "", + status: "LOADING", + }; + messageStore.addMessage(message); + if (connectionStore.currentConnectionCtx?.database) { let schema = ""; try { @@ -123,13 +138,13 @@ const ChatView = () => { role: CreatorRole.System, content: prompt, }); + const rawRes = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: formatedMessageList, }), }); - setIsRequesting(false); if (!rawRes.ok) { console.error(rawRes); @@ -140,7 +155,10 @@ const ChatView = () => { } catch (error) { // do nth } - toast.error(errorMessage); + messageStore.updateMessage(message.id, { + content: errorMessage, + status: "FAILED", + }); return; } @@ -150,17 +168,6 @@ const ChatView = () => { return; } - const message: Message = { - id: generateUUID(), - chatId: currentChat.id, - creatorId: currentChat.assistantId, - creatorRole: CreatorRole.Assistant, - createdAt: Date.now(), - content: "", - isGenerated: false, - }; - messageStore.addMessage(message); - const reader = data.getReader(); const decoder = new TextDecoder("utf-8"); let done = false; @@ -178,7 +185,7 @@ const ChatView = () => { done = readerDone; } messageStore.updateMessage(message.id, { - isGenerated: true, + status: "DONE", }); }; @@ -197,10 +204,9 @@ const ChatView = () => { ) : ( messageList.map((message) => ) )} - {isRequesting && }
- +
); diff --git a/src/components/EmptyView.tsx b/src/components/EmptyView.tsx index a026dd4..88f7295 100644 --- a/src/components/EmptyView.tsx +++ b/src/components/EmptyView.tsx @@ -36,7 +36,7 @@ const EmptyView = (props: Props) => { creatorRole: CreatorRole.User, createdAt: Date.now(), content: content, - isGenerated: true, + status: "DONE", }); await sendMessage(); }; diff --git a/src/types/message.ts b/src/types/message.ts index a7d1fd3..d927086 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -6,6 +6,8 @@ export enum CreatorRole { Assistant = "assistant", } +type MessageStatus = "LOADING" | "DONE" | "FAILED"; + export interface Message { id: Id; chatId: string; @@ -13,5 +15,5 @@ export interface Message { creatorRole: CreatorRole; createdAt: Timestamp; content: string; - isGenerated: boolean; + status: MessageStatus; }