From c0306ee6a720340ee1a7b15dc039b6897e9e8ed7 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 21 Jan 2025 15:18:23 -0600 Subject: [PATCH 1/7] chore(assistant): only write to slack in prod --- .../src/pages/api/paste-assistant-message.ts | 76 ++++++++++--------- .../pages/api/paste-assistant-thread/index.ts | 48 +++++++----- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/packages/paste-website/src/pages/api/paste-assistant-message.ts b/packages/paste-website/src/pages/api/paste-assistant-message.ts index 0f9c0181c8..e7a1017c36 100644 --- a/packages/paste-website/src/pages/api/paste-assistant-message.ts +++ b/packages/paste-website/src/pages/api/paste-assistant-message.ts @@ -20,6 +20,8 @@ const supabaseUrl = process.env.SUPABASE_URL; const supabaseServiceKey = process.env.SUPABASE_KEY; const slackChannelID = process.env.SLACK_CHANNEL_TMP_PASTE_ASSISTANT; +const isDev = process.env.NODE_ENV === "development"; + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // defaults to process.env["OPENAI_API_KEY"] }); @@ -94,7 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - if (slackChannelID === undefined || slackChannelID === "") { + if ((slackChannelID === undefined || slackChannelID === "") && !isDev) { logger.error(`${LOG_PREFIX} Slack channel ID is undefined`); rollbar.error(`${LOG_PREFIX} Slack channel ID is undefined`); res.status(500).json({ @@ -140,42 +142,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } - // get the slack thread ID for the message thread being updated so that we can post the new message to the correct slack thread - const supabaseClient = createClient(supabaseUrl, supabaseServiceKey); - const { data: slackThreadID, error: slackThreadIDError } = await supabaseClient - .from("assistant_threads") - .select("slack_thread_ts") - .eq("openai_thread_id", threadId); - - if (slackThreadIDError || slackThreadID == null) { - logger.error(`${LOG_PREFIX} Error getting slack thread ID for the message thread being updated`, { - slackThreadIDError, - }); - } else { - try { - // Post that new message to the correct slack thread for this assistant thread - const postToSlackResponse = await fetch(`${protocol}://${host}/api/post-to-slack`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - message: `New message was added to the thread: \n\n --- \n\n${message}`, - channelID: slackChannelID, - threadID: slackThreadID, - }), - }); - const slackResponseJSON = await postToSlackResponse.json(); - const slackResult = slackResponseJSON.result as ChatPostMessageResponse; - - logger.info(`${LOG_PREFIX} Posted to slack`, { - slackMessageCreated: slackResult.ts, - message: slackResult.message?.text, - }); - } catch (error) { - logger.error(`${LOG_PREFIX} Error sending slack message for thread being updated`, { - error, + if (!isDev) { + // get the slack thread ID for the message thread being updated so that we can post the new message to the correct slack thread + const supabaseClient = createClient(supabaseUrl, supabaseServiceKey); + const { data: slackThreadID, error: slackThreadIDError } = await supabaseClient + .from("assistant_threads") + .select("slack_thread_ts") + .eq("openai_thread_id", threadId); + + if (slackThreadIDError || slackThreadID == null) { + logger.error(`${LOG_PREFIX} Error getting slack thread ID for the message thread being updated`, { + slackThreadIDError, }); + } else { + try { + // Post that new message to the correct slack thread for this assistant thread + const postToSlackResponse = await fetch(`${protocol}://${host}/api/post-to-slack`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: `New message was added to the thread: \n\n --- \n\n${message}`, + channelID: slackChannelID, + threadID: slackThreadID, + }), + }); + const slackResponseJSON = await postToSlackResponse.json(); + const slackResult = slackResponseJSON.result as ChatPostMessageResponse; + + logger.info(`${LOG_PREFIX} Posted to slack`, { + slackMessageCreated: slackResult.ts, + message: slackResult.message?.text, + }); + } catch (error) { + logger.error(`${LOG_PREFIX} Error sending slack message for thread being updated`, { + error, + }); + } } } diff --git a/packages/paste-website/src/pages/api/paste-assistant-thread/index.ts b/packages/paste-website/src/pages/api/paste-assistant-thread/index.ts index 8bb9063d52..d5ff1ad382 100644 --- a/packages/paste-website/src/pages/api/paste-assistant-thread/index.ts +++ b/packages/paste-website/src/pages/api/paste-assistant-thread/index.ts @@ -21,6 +21,8 @@ const supabaseUrl = process.env.SUPABASE_URL; const supabaseServiceKey = process.env.SUPABASE_KEY; const slackChannelID = process.env.SLACK_CHANNEL_TMP_PASTE_ASSISTANT; +const isDev = process.env.NODE_ENV === "development"; + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // defaults to process.env["OPENAI_API_KEY"] }); @@ -73,39 +75,43 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) throw new ApplicationError("Missing environment variable SUPABASE_KEY"); } - if (!slackChannelID) { + if (!slackChannelID && !isDev) { throw new ApplicationError("Missing environment variable SLACK_CHANNEL_TMP_PASTE_ASSISTANT"); } const newThread = await createThread(); logger.info(`${LOG_PREFIX} Created thread`, { newThread }); - // post to slack - const postToSlackResponse = await fetch(`${protocol}://${host}/api/post-to-slack`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - message: `New Paste Assistant thread created: ${newThread.id}`, - channelID: slackChannelID, - }), - }); - - const slackResponseJSON = await postToSlackResponse.json(); - const slackResult = slackResponseJSON.result as ChatPostMessageResponse; - - logger.info(`${LOG_PREFIX} Posted to slack`, { - slackMessageCreated: slackResult.ts, - message: slackResult.message?.text, - }); + let slackResult: ChatPostMessageResponse | undefined; + + if (!isDev) { + // post to slack + const postToSlackResponse = await fetch(`${protocol}://${host}/api/post-to-slack`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: `New Paste Assistant thread created: ${newThread.id}`, + channelID: slackChannelID, + }), + }); + + const slackResponseJSON = await postToSlackResponse.json(); + slackResult = slackResponseJSON.result as ChatPostMessageResponse; + + logger.info(`${LOG_PREFIX} Posted to slack`, { + slackMessageCreated: slackResult.ts, + message: slackResult.message?.text, + }); + } // save to supabase const supabaseClient = createClient(supabaseUrl, supabaseServiceKey); const { data: newAssistantThreadData, error: newAssistantThreadError } = await supabaseClient .from("assistant_threads") // eslint-disable-next-line camelcase - .insert([{ openai_thread_id: newThread.id, slack_thread_ts: slackResult.ts }]); + .insert([{ openai_thread_id: newThread.id, slack_thread_ts: slackResult?.ts }]); if (newAssistantThreadError) { throw new ApplicationError("Failed to store new thread", newAssistantThreadError); From a7f97cebfdbf83fc49372814a2250b197bbcf220 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Wed, 22 Jan 2025 08:22:53 -0600 Subject: [PATCH 2/7] fix(assistant): fix the vercel error sometimes showing --- .../paste-website/src/components/assistant/AssistantMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/paste-website/src/components/assistant/AssistantMessage.tsx b/packages/paste-website/src/components/assistant/AssistantMessage.tsx index 5b22f59a9e..c0dab02408 100644 --- a/packages/paste-website/src/components/assistant/AssistantMessage.tsx +++ b/packages/paste-website/src/components/assistant/AssistantMessage.tsx @@ -12,7 +12,7 @@ export const AssistantMessage: React.FC<{ threadMessage: ThreadMessage }> = ({ t PasteBot - {threadMessage.content[0].type === "text" && ( + {threadMessage.content.length > 0 && threadMessage.content[0]?.type === "text" && ( {threadMessage.content[0].text.value} )} From 9c2f8277bdffd4e6e66433063f8d265033da4bc3 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Wed, 22 Jan 2025 10:23:24 -0600 Subject: [PATCH 3/7] feat(assistant): use animations and composer disabled states --- .../components/assistant/AssistantCanvas.tsx | 57 +++++++++++++++++-- .../assistant/AssistantComposer.tsx | 18 ++++-- .../components/assistant/AssistantLayout.tsx | 2 +- .../assistant/AssistantMarkdown.tsx | 18 +++--- .../components/assistant/AssistantMessage.tsx | 24 +++++--- .../src/components/assistant/UserMessage.tsx | 10 +++- .../paste-website/src/pages/assistant.tsx | 2 +- .../src/stores/assistantRunStore.ts | 16 ++++-- 8 files changed, 114 insertions(+), 33 deletions(-) diff --git a/packages/paste-website/src/components/assistant/AssistantCanvas.tsx b/packages/paste-website/src/components/assistant/AssistantCanvas.tsx index 725c9fae0e..920b68119d 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..7c3530c218 100644 --- a/packages/paste-website/src/components/assistant/AssistantComposer.tsx +++ b/packages/paste-website/src/components/assistant/AssistantComposer.tsx @@ -13,6 +13,9 @@ import * as React from "react"; import { useAssistantThreadsStore } from "../../stores/assistantThreadsStore"; import useStoreWithLocalStorage from "../../stores/useStore"; import { EnterKeySubmitPlugin } from "./EnterKeySubmitPlugin"; +import { useAssistantRunStore } from "../../stores/assistantRunStore"; +import { useShallow } from "zustand/react/shallow"; +import { useIsMutating } from "@tanstack/react-query"; export const AssistantComposer: React.FC<{ onMessageCreation: (message: string, selectedThread?: string) => void }> = ({ onMessageCreation, @@ -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 = !!(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()} />