From 71dae75521c4170fdcfe4bf0b888f5ea17b40994 Mon Sep 17 00:00:00 2001 From: Daniel Garza Date: Sun, 4 Aug 2024 14:59:53 +0200 Subject: [PATCH 1/2] create button to stop streaming of response --- app/frontend/src/api/api.ts | 5 +- .../QuestionInput/QuestionInput.tsx | 19 +++++-- app/frontend/src/pages/ask/Ask.tsx | 2 +- app/frontend/src/pages/chat/Chat.tsx | 57 ++++++++++++++++++- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index 8c424f675c..62aa4df254 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -38,7 +38,7 @@ export async function askApi(request: ChatAppRequest, idToken: string | undefine return parsedResponse as ChatAppResponse; } -export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined): Promise { +export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined, signal: AbortSignal): Promise { let url = `${BACKEND_URI}/chat`; if (shouldStream) { url += "/stream"; @@ -47,7 +47,8 @@ export async function chatApi(request: ChatAppRequest, shouldStream: boolean, id return await fetch(url, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, - body: JSON.stringify(request) + body: JSON.stringify(request), + signal: signal }); } diff --git a/app/frontend/src/components/QuestionInput/QuestionInput.tsx b/app/frontend/src/components/QuestionInput/QuestionInput.tsx index b76263a23d..853b62abf1 100644 --- a/app/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/app/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useContext } from "react"; import { Stack, TextField } from "@fluentui/react"; import { Button, Tooltip } from "@fluentui/react-components"; -import { Send28Filled } from "@fluentui/react-icons"; +import { Send28Filled, Stop24Filled } from "@fluentui/react-icons"; + import { useMsal } from "@azure/msal-react"; import styles from "./QuestionInput.module.css"; @@ -16,9 +17,11 @@ interface Props { placeholder?: string; clearOnSend?: boolean; showSpeechInput?: boolean; + onStop?: () => void; + isStreaming: boolean; } -export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend, initQuestion, showSpeechInput }: Props) => { +export const QuestionInput = ({ onSend, onStop, disabled, placeholder, clearOnSend, initQuestion, showSpeechInput, isStreaming }: Props) => { const [question, setQuestion] = useState(""); const { loggedIn } = useContext(LoginContext); @@ -74,9 +77,15 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend, init onKeyDown={onEnterPress} />
- -
{showSpeechInput && } diff --git a/app/frontend/src/pages/ask/Ask.tsx b/app/frontend/src/pages/ask/Ask.tsx index 84d1ea87e3..22707f4b1c 100644 --- a/app/frontend/src/pages/ask/Ask.tsx +++ b/app/frontend/src/pages/ask/Ask.tsx @@ -246,7 +246,7 @@ export function Component(): JSX.Element { initQuestion={question} onSend={question => makeApiRequest(question)} showSpeechInput={showSpeechInput} - /> + isStreaming={false} />
diff --git a/app/frontend/src/pages/chat/Chat.tsx b/app/frontend/src/pages/chat/Chat.tsx index 220526caf4..94dcda19c9 100644 --- a/app/frontend/src/pages/chat/Chat.tsx +++ b/app/frontend/src/pages/chat/Chat.tsx @@ -60,6 +60,8 @@ const Chat = () => { const [isLoading, setIsLoading] = useState(false); const [isStreaming, setIsStreaming] = useState(false); + const [partialResponse, setPartialResponse] = useState(""); + const [abortController, setAbortController] = useState(null); const [error, setError] = useState(); const [activeCitation, setActiveCitation] = useState(); @@ -94,7 +96,7 @@ const Chat = () => { }); }; - const handleAsyncRequest = async (question: string, answers: [string, ChatAppResponse][], responseBody: ReadableStream) => { + const handleAsyncRequest = async (question: string, answers: [string, ChatAppResponse][], responseBody: ReadableStream, signal: AbortSignal) => { let answer: string = ""; let askResponse: ChatAppResponse = {} as ChatAppResponse; @@ -102,6 +104,7 @@ const Chat = () => { return new Promise(resolve => { setTimeout(() => { answer += newContent; + setPartialResponse(answer); const latestResponse: ChatAppResponse = { ...askResponse, message: { content: answer, role: askResponse.message.role } @@ -114,6 +117,9 @@ const Chat = () => { try { setIsStreaming(true); for await (const event of readNDJSONStream(responseBody)) { + if (signal.aborted) { + break; + } if (event["context"] && event["context"]["data_points"]) { event["message"] = event["delta"]; askResponse = event as ChatAppResponse; @@ -127,8 +133,11 @@ const Chat = () => { throw Error(event["error"]); } } + } catch (e) { + console.error("error in handleAsyncRequest: ", e); } finally { setIsStreaming(false); + setPartialResponse(""); } const fullResponse: ChatAppResponse = { ...askResponse, @@ -141,6 +150,8 @@ const Chat = () => { const { loggedIn } = useContext(LoginContext); const makeApiRequest = async (question: string) => { + const controller = new AbortController(); + setAbortController(controller); lastQuestionRef.current = question; error && setError(undefined); @@ -182,12 +193,12 @@ const Chat = () => { session_state: answers.length ? answers[answers.length - 1][1].session_state : null }; - const response = await chatApi(request, shouldStream, token); + const response = await chatApi(request, shouldStream, token, controller.signal); if (!response.body) { throw Error("No response body"); } if (shouldStream) { - const parsedResponse: ChatAppResponse = await handleAsyncRequest(question, answers, response.body); + const parsedResponse: ChatAppResponse = await handleAsyncRequest(question, answers, response.body, controller.signal); setAnswers([...answers, [question, parsedResponse]]); } else { const parsedResponse: ChatAppResponseOrError = await response.json(); @@ -212,6 +223,7 @@ const Chat = () => { setStreamedAnswers([]); setIsLoading(false); setIsStreaming(false); + setPartialResponse(""); }; useEffect(() => chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }), [isLoading]); @@ -310,6 +322,16 @@ const Chat = () => { setSelectedAnswer(index); }; + const onStopClick = async () => { + try { + if (abortController) { + abortController.abort(); + } + } catch (e) { + console.log("An error occurred trying to stop the stream: ", e); + } + }; + // IDs for form labels and their associated callouts const promptTemplateId = useId("promptTemplate"); const promptTemplateFieldId = useId("promptTemplateField"); @@ -378,6 +400,33 @@ const Chat = () => {
))} + {partialResponse && !isStreaming && ( +
+ +
+ {}} + onThoughtProcessClicked={() => {}} + onSupportingContentClicked={() => {}} + onFollowupQuestionClicked={() => {}} + showFollowupQuestions={false} + speechUrl={null} + /> +
+
+ )} {!isStreaming && answers.map((answer, index) => (
@@ -427,6 +476,8 @@ const Chat = () => { disabled={isLoading} onSend={question => makeApiRequest(question)} showSpeechInput={showSpeechInput} + isStreaming={isStreaming} + onStop={onStopClick} />
From ac7a78be99ec5942a047520f23eaccf6eaf968f5 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 2 Sep 2024 04:39:27 -0700 Subject: [PATCH 2/2] Update ja translation --- app/frontend/src/locales/ja/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/locales/ja/translation.json b/app/frontend/src/locales/ja/translation.json index a8cbd05255..9799e9a06d 100644 --- a/app/frontend/src/locales/ja/translation.json +++ b/app/frontend/src/locales/ja/translation.json @@ -42,7 +42,7 @@ "tooltips":{ "submitQuestion": "質問を送信", - "stopStreaming": "停止串流", + "stopStreaming": "ストリーミングを停止", "askWithVoice": "音声で質問", "stopRecording": "質問の記録を停止", "showThoughtProcess": "思考プロセスの表示",