diff --git a/packages/paste-website/src/components/assistant/AssistantCanvas.tsx b/packages/paste-website/src/components/assistant/AssistantCanvas.tsx index 725c9fae0e..7b560a1a65 100644 --- a/packages/paste-website/src/components/assistant/AssistantCanvas.tsx +++ b/packages/paste-website/src/components/assistant/AssistantCanvas.tsx @@ -18,9 +18,12 @@ type AssistantCanvasProps = { export const AssistantCanvas: React.FC = ({ selectedThreadID }) => { const [mounted, setMounted] = React.useState(false); + const [isAnimating, setIsAnimating] = React.useState(false); + const [userInterctedScroll, setUserInteractedScroll] = React.useState(false); + const messages = useAssistantMessagesStore(useShallow((state) => state.messages)); const setMessages = useAssistantMessagesStore(useShallow((state) => state.setMessages)); - const activeRun = useAssistantRunStore(useShallow((state) => state.activeRun)); + const { activeRun, lastActiveRun, clearLastActiveRun } = useAssistantRunStore(useShallow((state) => state)); const isCreatingAResponse = useIsMutating({ mutationKey: ["create-assistant-run"] }); const memoedMessages = React.useMemo(() => messages, [messages]); @@ -50,14 +53,46 @@ export const AssistantCanvas: React.FC = ({ selectedThread setMounted(true); }, []); + const scrollToChatEnd = (): void => { + const scrollPosition: any = scrollerRef.current; + const scrollHeight: any = loggerRef.current; + scrollPosition?.scrollTo({ top: scrollHeight.scrollHeight, behavior: "smooth" }); + }; + // scroll to bottom of chat log when new messages are added React.useEffect(() => { if (!mounted || !loggerRef.current) return; - scrollerRef.current?.scrollTo({ top: loggerRef.current.scrollHeight, behavior: "smooth" }); + scrollToChatEnd(); }, [memoedMessages, mounted]); + const onAnimationEnd = (): void => { + setIsAnimating(false); + setUserInteractedScroll(false); + // avoid reanimating the same message + clearLastActiveRun(); + }; + + const onAnimationStart = (): void => { + setUserInteractedScroll(false); + setIsAnimating(true); + }; + + const userScrolled = (): void => setUserInteractedScroll(true); + + React.useEffect(() => { + scrollerRef.current?.addEventListener("wheel", userScrolled); + scrollerRef.current?.addEventListener("touchmove", userScrolled); + + const interval = setInterval(() => isAnimating && !userInterctedScroll && scrollToChatEnd(), 5); + return () => { + if (interval) clearInterval(interval); + scrollerRef.current?.removeEventListener("wheel", userScrolled); + scrollerRef.current?.removeEventListener("touchmove", userScrolled); + }; + }, [isAnimating, userInterctedScroll]); + return ( - + {activeRun != null && } @@ -94,11 +129,21 @@ export const AssistantCanvas: React.FC = ({ selectedThread Your conversations are not used to train OpenAI's models, but are stored by OpenAI. - {messages?.map((threadMessage): React.ReactNode => { + {messages?.map((threadMessage, index): React.ReactNode => { if (threadMessage.role === "assistant") { - return ; + return ( + + ); } - return ; + return ; })} {(isCreatingAResponse || activeRun != null) && } diff --git a/packages/paste-website/src/components/assistant/AssistantComposer.tsx b/packages/paste-website/src/components/assistant/AssistantComposer.tsx index 3e1ad0b75d..91478213ac 100644 --- a/packages/paste-website/src/components/assistant/AssistantComposer.tsx +++ b/packages/paste-website/src/components/assistant/AssistantComposer.tsx @@ -1,3 +1,4 @@ +import { useIsMutating } from "@tanstack/react-query"; import { Button } from "@twilio-paste/button"; import { ChatComposer, ChatComposerActionGroup, ChatComposerContainer } from "@twilio-paste/chat-composer"; import { SendIcon } from "@twilio-paste/icons/esm/SendIcon"; @@ -9,7 +10,9 @@ import { type LexicalEditor, } from "@twilio-paste/lexical-library"; import * as React from "react"; +import { useShallow } from "zustand/react/shallow"; +import { useAssistantRunStore } from "../../stores/assistantRunStore"; import { useAssistantThreadsStore } from "../../stores/assistantThreadsStore"; import useStoreWithLocalStorage from "../../stores/useStore"; import { EnterKeySubmitPlugin } from "./EnterKeySubmitPlugin"; @@ -20,9 +23,12 @@ export const AssistantComposer: React.FC<{ onMessageCreation: (message: string, const [message, setMessage] = React.useState(""); const threadsStore = useStoreWithLocalStorage(useAssistantThreadsStore, (state) => state); const selectedThread = threadsStore?.selectedThreadID; - + const { activeRun } = useAssistantRunStore(useShallow((state) => state)); + const isCreatingAResponse = useIsMutating({ mutationKey: ["create-assistant-run"] }); const editorInstanceRef = React.useRef(null); + const isLoading = Boolean(isCreatingAResponse || activeRun != null); + const handleComposerChange = (editorState: EditorState): void => { editorState.read(() => { const text = $getRoot().getTextContent(); @@ -49,21 +55,25 @@ export const AssistantComposer: React.FC<{ onMessageCreation: (message: string, throw error; }, }} + disabled={isLoading} ariaLabel="Message" placeholder="Type here..." onChange={handleComposerChange} editorInstanceRef={editorInstanceRef} > - + !isLoading && submitMessage()} />