diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 4acda9b8a99e74f3e66184e891d0284462535e65..5869a81f795f72156436fa911b52eea5e46c4935 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -1,66 +1,56 @@ -import { useEffect, useRef, memo } from "react"; +import { memo, forwardRef } from "react"; import { AlertTriangle } from "react-feather"; import Jazzicon from "../../../../UserIcon"; import renderMarkdown from "../../../../../utils/chat/markdown"; import { userFromStorage } from "../../../../../utils/request"; import Citations from "../Citation"; -function HistoricalMessage({ - message, - role, - workspace, - sources = [], - error = false, -}) { - const replyRef = useRef(null); - useEffect(() => { - if (replyRef.current) - replyRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); - }, [replyRef.current]); +const HistoricalMessage = forwardRef( + ({ message, role, workspace, sources = [], error = false }, ref) => { + if (role === "user") { + return ( + <div className="flex justify-end mb-4 items-start"> + <div className="mr-2 py-1 px-4 w-fit md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm"> + <span + className={`inline-block p-2 rounded-lg whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base`} + > + {message} + </span> + </div> + <Jazzicon size={30} user={{ uid: userFromStorage()?.username }} /> + </div> + ); + } - if (role === "user") { - return ( - <div className="flex justify-end mb-4 items-start"> - <div className="mr-2 py-1 px-4 w-fit md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm"> - <span - className={`inline-block p-2 rounded-lg whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base`} - > - {message} - </span> + if (error) { + return ( + <div className="flex justify-start mb-4 items-end"> + <Jazzicon size={30} user={{ uid: workspace.slug }} /> + <div className="ml-2 max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> + <span + className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`} + > + <AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could not + respond to message. + </span> + </div> </div> - <Jazzicon size={30} user={{ uid: userFromStorage()?.username }} /> - </div> - ); - } + ); + } - if (error) { return ( - <div className="flex justify-start mb-4 items-end"> + <div ref={ref} className="flex justify-start items-end mb-4"> <Jazzicon size={30} user={{ uid: workspace.slug }} /> - <div className="ml-2 max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> + <div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> <span - className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`} - > - <AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could not - respond to message. - </span> + className="no-scroll whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1" + dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }} + /> + <Citations sources={sources} /> </div> </div> ); } - - return ( - <div ref={replyRef} className="flex justify-start items-end mb-4"> - <Jazzicon size={30} user={{ uid: workspace.slug }} /> - <div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> - <span - className="no-scroll whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1" - dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }} - /> - <Citations sources={sources} /> - </div> - </div> - ); -} +); export default memo(HistoricalMessage); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx index a82c25a4815731efabc14d5527a42f806b1505d8..3ef308e2f799c4ddfd3e1aceebb0b49788f4ace9 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx @@ -1,71 +1,61 @@ -import { memo, useEffect, useRef } from "react"; +import { forwardRef, memo } from "react"; import { AlertTriangle } from "react-feather"; import Jazzicon from "../../../../UserIcon"; import renderMarkdown from "../../../../../utils/chat/markdown"; import Citations from "../Citation"; -function PromptReply({ - uuid, - reply, - pending, - error, - workspace, - sources = [], - closed = true, -}) { - const replyRef = useRef(null); - useEffect(() => { - if (replyRef.current) - replyRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); - }, [replyRef.current]); +const PromptReply = forwardRef( + ( + { uuid, reply, pending, error, workspace, sources = [], closed = true }, + ref + ) => { + if (!reply && !sources.length === 0 && !pending && !error) return null; + if (pending) { + return ( + <div + ref={ref} + className="chat__message flex justify-start mb-4 items-end" + > + <Jazzicon size={30} user={{ uid: workspace.slug }} /> + <div className="ml-2 pt-2 px-6 w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> + <span className={`inline-block p-2`}> + <div className="dot-falling"></div> + </span> + </div> + </div> + ); + } - if (!reply && !sources.length === 0 && !pending && !error) return null; - if (pending) { - return ( - <div className="chat__message flex justify-start mb-4 items-end"> - <Jazzicon size={30} user={{ uid: workspace.slug }} /> - <div className="ml-2 pt-2 px-6 w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> - <span className={`inline-block p-2`}> - <div className="dot-falling"></div> - </span> + if (error) { + return ( + <div className="chat__message flex justify-start mb-4 items-center"> + <Jazzicon size={30} user={{ uid: workspace.slug }} /> + <div className="ml-2 py-3 px-4 rounded-br-3xl rounded-tr-3xl rounded-tl-xl text-slate-100 "> + <div className="bg-red-50 text-red-500 rounded-lg w-fit flex flex-col p-2"> + <span className={`inline-block`}> + <AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could + not respond to message. + </span> + <span className="text-xs">Reason: {error || "unknown"}</span> + </div> + </div> </div> - </div> - ); - } + ); + } - if (error) { return ( - <div className="chat__message flex justify-start mb-4 items-center"> + <div key={uuid} ref={ref} className="mb-4 flex justify-start items-end"> <Jazzicon size={30} user={{ uid: workspace.slug }} /> - <div className="ml-2 py-3 px-4 rounded-br-3xl rounded-tr-3xl rounded-tl-xl text-slate-100 "> - <div className="bg-red-50 text-red-500 rounded-lg w-fit flex flex-col p-2"> - <span className={`inline-block`}> - <AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could not - respond to message. - </span> - <span className="text-xs">Reason: {error || "unknown"}</span> - </div> + <div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> + <span + className="whitespace-pre-line text-slate-800 dark:text-slate-200 flex flex-col gap-y-1 font-[500] md:font-semibold text-sm md:text-base" + dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }} + /> + <Citations sources={sources} /> </div> </div> ); } - - return ( - <div - key={uuid} - ref={replyRef} - className="mb-4 flex justify-start items-end" - > - <Jazzicon size={30} user={{ uid: workspace.slug }} /> - <div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> - <span - className="whitespace-pre-line text-slate-800 dark:text-slate-200 flex flex-col gap-y-1 font-[500] md:font-semibold text-sm md:text-base" - dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }} - /> - <Citations sources={sources} /> - </div> - </div> - ); -} +); export default memo(PromptReply); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 6e400c03f6f9e454486eff3a1d972b8d3f042ab5..201cbc7ddf5e18d89d78e3fca4b214c6a8b9a980 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -1,8 +1,19 @@ import { Frown } from "react-feather"; import HistoricalMessage from "./HistoricalMessage"; import PromptReply from "./PromptReply"; +import { useEffect, useRef } from "react"; export default function ChatHistory({ history = [], workspace }) { + const replyRef = useRef(null); + + useEffect(() => { + if (replyRef.current) { + setTimeout(() => { + replyRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); + }, 700); + } + }, [history]); + if (history.length === 0) { return ( <div className="flex flex-col h-[89%] md:mt-0 pb-5 w-full justify-center items-center"> @@ -22,48 +33,37 @@ export default function ChatHistory({ history = [], workspace }) { className="h-[89%] pb-[100px] md:pt-[50px] md:pt-0 md:pb-5 mx-2 md:mx-0 overflow-y-scroll flex flex-col justify-start no-scroll" id="chat-history" > - {history.map( - ( - { - uuid = null, - content, - sources = [], - role, - closed = true, - pending = false, - error = false, - animate = false, - }, - index - ) => { - const isLastBotReply = - index === history.length - 1 && role === "assistant"; - if (isLastBotReply && animate) { - return ( - <PromptReply - key={uuid} - uuid={uuid} - reply={content} - pending={pending} - sources={sources} - error={error} - workspace={workspace} - closed={closed} - /> - ); - } + {history.map((props, index) => { + const isLastMessage = index === history.length - 1; + + if (props.role === "assistant" && props.animate) { return ( - <HistoricalMessage - key={index} - message={content} - role={role} + <PromptReply + key={props.uuid} + ref={isLastMessage ? replyRef : null} + uuid={props.uuid} + reply={props.content} + pending={props.pending} + sources={props.sources} + error={props.error} workspace={workspace} - sources={sources} - error={error} + closed={props.closed} /> ); } - )} + + return ( + <HistoricalMessage + key={index} + ref={isLastMessage ? replyRef : null} + message={props.content} + role={props.role} + workspace={workspace} + sources={props.sources} + error={props.error} + /> + ); + })} </div> ); }