From 2ab85b8730925a78ecfc56312177cdafd798dbf3 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Tue, 18 Feb 2025 18:24:49 -0800 Subject: [PATCH] clean up code and improve feedback button handling - dont use special method handleFeedbackClick for sending feedback buttons - handle SSE close and error events - show linebreaks in markdown --- src/api/streaming.ts | 26 ++++- src/contexts/MessagesContext.tsx | 62 ++++------- .../copilot/components/ChatInput/index.tsx | 9 +- .../components/Messages/IncomingMsg.tsx | 104 +++++++++++++----- .../copilot/components/Messages/helpers.tsx | 10 +- .../copilot/components/Messages/index.tsx | 5 +- 6 files changed, 135 insertions(+), 81 deletions(-) diff --git a/src/api/streaming.ts b/src/api/streaming.ts index 9993d0e..21e5ac2 100644 --- a/src/api/streaming.ts +++ b/src/api/streaming.ts @@ -46,17 +46,35 @@ export const getDataFromStream = (sseUrl: string, setterFn: any) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error window.GooeyEventSource = evtSource; + + evtSource.addEventListener("close", () => { + // close the event source + evtSource.close(); + // set the state to null + setterFn(null); + }); + + evtSource.addEventListener("error", (event: MessageEvent) => { + // parse the error message as JSON + const { detail } = JSON.parse(event.data); + // display the error message + setterFn({ + type: STREAM_MESSAGE_TYPES.MESSAGE_PART, + text: `

${detail}

`, + }); + // close the event source + evtSource.close(); + }); + evtSource.onmessage = (event) => { // parse the message as JSON const data = JSON.parse(event.data); + // update the state with the streamed message + setterFn(data); // check if the message is the final response if (data.type === STREAM_MESSAGE_TYPES.FINAL_RESPONSE) { // close the stream - setterFn(data); evtSource.close(); - } else { - // update the state with the streamed message - setterFn(data); } }; }; diff --git a/src/contexts/MessagesContext.tsx b/src/contexts/MessagesContext.tsx index 16f1a54..284b640 100644 --- a/src/contexts/MessagesContext.tsx +++ b/src/contexts/MessagesContext.tsx @@ -68,7 +68,7 @@ const MessagesContextProvider = (props: any) => { const conversationId = lastResponse?.conversation_id; setIsSendingMessage(true); const newQuery = createNewQuery(payload); - sendPrompt({ + sendPayload({ ...payload, conversation_id: conversationId, citation_style: CITATION_STYLE, @@ -109,6 +109,21 @@ const MessagesContextProvider = (props: any) => { const updateStreamedMessage = useCallback( (payload: any) => { setMessages((prev: any) => { + // stream close + if (!payload) { + const newMessages = new Map(prev); + const lastResponseId: any = Array.from(prev.keys()).pop(); // last message id + const prevMessage = prev.get(lastResponseId); + newMessages.set(lastResponseId, { + ...prevMessage, + output_text: [prevMessage?.text ?? ""], + type: STREAM_MESSAGE_TYPES.FINAL_RESPONSE, + status: "completed", + }); + setIsReceiving(false); + return newMessages; + } + // stream start if (payload?.type === STREAM_MESSAGE_TYPES.CONVERSATION_START) { setIsSendingMessage(false); @@ -167,11 +182,16 @@ const MessagesContextProvider = (props: any) => { const lastResponseId: any = Array.from(prev.keys()).pop(); // last messages id const prevMessage = prev.get(lastResponseId); const text = (prevMessage?.text || "") + (payload.text || ""); + const buttons = [ + ...(prevMessage?.buttons || []), + ...(payload.buttons || []), + ]; newConversations.set(lastResponseId, { ...prevMessage, ...payload, id: currentStreamRef.current, text, + buttons, }); return newConversations; } @@ -182,17 +202,15 @@ const MessagesContextProvider = (props: any) => { [config?.integration_id, handleAddConversation, scrollToMessage] ); - const sendPrompt = async (payload: IncomingMsg) => { + const sendPayload = async (payload: IncomingMsg) => { try { - let audioUrl = ""; if (payload?.input_audio) { // upload audio file to gooey const file = new File( [payload.input_audio], `gooey-widget-recording-${uuidv4()}.webm` ); - audioUrl = await uploadFileToGooey(file as File); - payload.input_audio = audioUrl; + payload.input_audio = await uploadFileToGooey(file as File); } payload = { ...config?.payload, @@ -207,8 +225,7 @@ const MessagesContextProvider = (props: any) => { ); getDataFromStream(streamUrl, updateStreamedMessage); // setLoading false in updateStreamedMessage - } catch (err) { - console.error("Api Failed!", err); + } finally { setIsSendingMessage(false); } }; @@ -276,35 +293,6 @@ const MessagesContextProvider = (props: any) => { setIsSendingMessage(false); }, [isReceiving, isSending, messages]); - const handleFeedbackClick = (button_id: string, context_msg_id: string) => { - createStreamApi( - { - button_pressed: { - button_id, - context_msg_id, - }, - integration_id: config?.integration_id, - user_id: currentUserId, - }, - apiSource.current - ); - setMessages((prev: any) => { - const newConversations = new Map(prev); - const prevMessage = prev.get(context_msg_id); - const newButtons = prevMessage.buttons.map((button: any) => { - if (button.id === button_id) { - return { ...button, isPressed: true }; - } - return undefined; // hide the other buttons - }); - newConversations.set(context_msg_id, { - ...prevMessage, - buttons: newButtons, - }); - return newConversations; - }); - }; - const setActiveConversation = useCallback( async (conversation: Conversation) => { if (isSending || isReceiving) cancelApiCall(); @@ -348,7 +336,6 @@ const MessagesContextProvider = (props: any) => { }; const valueMessages = { - sendPrompt, messages, isSending, initializeQuery, @@ -357,7 +344,6 @@ const MessagesContextProvider = (props: any) => { scrollMessageContainer, scrollContainerRef, isReceiving, - handleFeedbackClick, conversations, setActiveConversation, currentConversationId: currentConversation.current?.id || null, diff --git a/src/widgets/copilot/components/ChatInput/index.tsx b/src/widgets/copilot/components/ChatInput/index.tsx index 75bccb2..83cfca3 100644 --- a/src/widgets/copilot/components/ChatInput/index.tsx +++ b/src/widgets/copilot/components/ChatInput/index.tsx @@ -179,7 +179,8 @@ const ChatInput = () => { onChange={handleInputChange} onKeyDown={handlePressEnter} className={clsx( - "br-large b-1 font_16_500 bg-white gpt-10 gpb-10 gpr-40 flex-1 gm-0", isLeftButtons ? "gpl-32" : "gpl-12" + "br-large b-1 font_16_500 bg-white gpt-10 gpb-10 gpr-40 flex-1 gm-0", + isLeftButtons ? "gpl-32" : "gpl-12" )} placeholder={`Message ${config.branding.name || ""}`} > @@ -187,7 +188,11 @@ const ChatInput = () => { {/* Left icons */} {isLeftButtons && (
- +
diff --git a/src/widgets/copilot/components/Messages/IncomingMsg.tsx b/src/widgets/copilot/components/Messages/IncomingMsg.tsx index f0e8a37..3892135 100644 --- a/src/widgets/copilot/components/Messages/IncomingMsg.tsx +++ b/src/widgets/copilot/components/Messages/IncomingMsg.tsx @@ -1,4 +1,4 @@ -import { useSystemContext } from "src/contexts/hooks"; +import { useMessagesContext, useSystemContext } from "src/contexts/hooks"; import { STREAM_MESSAGE_TYPES } from "src/api/streaming"; import ResponseLoader from "../Loader"; @@ -37,28 +37,78 @@ export const BotMessageLayout = (props: Record) => { ); }; -const FeedbackButtons = ({ data, onFeedbackClick }: any) => { +type ReplyButton = { + id: string; + title: string; + isPressed?: boolean; +}; + +const FeedbackButtons = ({ + data, +}: { + data: { + buttons: ReplyButton[]; + bot_message_id: string; + }; +}) => { const { buttons, bot_message_id } = data; + const { initializeQuery }: any = useMessagesContext(); if (!buttons) return null; - return ( -
- {buttons.map( - (button: any) => - !!button && ( - - ), - )} -
- ); + const children = buttons + .map( + (button) => + button && ( + { + if (button.isPressed) return; + initializeQuery({ + button_pressed: { + button_id: button.id, + context_msg_id: bot_message_id, + }, + }); + }} + /> + ) + ) + .filter(Boolean); + return
{children}
; +}; + +const FeedbackButton = ({ + button, + onClick, +}: { + button: ReplyButton; + onClick: () => void; +}) => { + let icon = getFeedbackButtonIcon(button.id, button.isPressed || false); + if (icon) { + return ( + + ); + } else { + return ( + + ); + } }; const IncomingMsg = memo( @@ -68,7 +118,6 @@ const IncomingMsg = memo( showSources: boolean; linkColor: string; autoPlay: boolean | undefined; - onFeedbackClick: (buttonId: string, botMessageId: string) => void; }) => { const { output_audio = [], @@ -85,7 +134,7 @@ const IncomingMsg = memo( const parsedElements = formatTextResponse( props.data, props?.linkColor, - props?.showSources, + props?.showSources ); if (!parsedElements) return ; @@ -96,7 +145,7 @@ const IncomingMsg = memo(
@@ -125,10 +174,7 @@ const IncomingMsg = memo(
)} {!isStreaming && props?.data?.buttons && ( - + )} {props.showSources && !!references.length && ( @@ -136,7 +182,7 @@ const IncomingMsg = memo( )} ); - }, + } ); export default IncomingMsg; diff --git a/src/widgets/copilot/components/Messages/helpers.tsx b/src/widgets/copilot/components/Messages/helpers.tsx index 95d1a2c..25b9d2b 100644 --- a/src/widgets/copilot/components/Messages/helpers.tsx +++ b/src/widgets/copilot/components/Messages/helpers.tsx @@ -21,7 +21,7 @@ const GOOEY_META_SCRAPPER_API = "https://metascraper.gooey.ai"; export const findSourceIcon = ( contentType: string, url: string, - size: number = 12, + size: number = 12 ): JSX.ElementType | null => { const urlLower = url.toLowerCase(); // try to guess from url first @@ -123,7 +123,7 @@ export function extractMainDomain(url: string) { export const fetchUrlMeta = async (url: string): Promise => { try { const response: any = await axios.get( - `${GOOEY_META_SCRAPPER_API}/fetchUrlMeta?url=${url}`, + `${GOOEY_META_SCRAPPER_API}/fetchUrlMeta?url=${url}` ); return response?.data; } catch (err) { @@ -268,13 +268,13 @@ const customizedLinks = (reactNode: any, domNode: any, data: any) => { export const formatTextResponse = ( data: any, linkColor: string, - showSources: boolean, + showSources: boolean ) => { const body = getOutputText(data); if (!body) return ""; const rawHtml = marked.parse(body, { async: false, - breaks: false, + breaks: true, extensions: null, gfm: true, hooks: null, @@ -285,7 +285,7 @@ export const formatTextResponse = ( }); const parsedElements = parse( rawHtml as string, - getReactParserOptions({ ...data, showSources, linkColor }), + getReactParserOptions({ ...data, showSources, linkColor }) ); return parsedElements; }; diff --git a/src/widgets/copilot/components/Messages/index.tsx b/src/widgets/copilot/components/Messages/index.tsx index 5060234..bb2f58b 100644 --- a/src/widgets/copilot/components/Messages/index.tsx +++ b/src/widgets/copilot/components/Messages/index.tsx @@ -9,7 +9,7 @@ import SpinLoader from "src/components/shared/SpinLoader"; const Responses = (props: any) => { const { config } = useSystemContext(); - const { handleFeedbackClick, preventAutoplay }: any = useMessagesContext(); + const { preventAutoplay }: any = useMessagesContext(); const que = useMemo(() => props.queue, [props]); const msgs = props.data; @@ -34,7 +34,6 @@ const Responses = (props: any) => { id={id} showSources={config?.showSources || false} linkColor={config?.branding?.colors?.primary || "initial"} - onFeedbackClick={handleFeedbackClick} autoPlay={preventAutoplay ? false : config?.autoPlayResponses} /> ); @@ -72,7 +71,7 @@ const Messages = () => { ref={scrollContainerRef} className={clsx( "flex-1 bg-white gpt-16 gpb-16 gpr-16 gpb-16 d-flex flex-col", - isEmpty ? "justify-end" : "justify-start", + isEmpty ? "justify-end" : "justify-start" )} style={{ overflowY: "auto" }} >