diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index f2cd36507e..1fb41b5c59 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -41,7 +41,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"; @@ -50,7 +50,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 5612a3475b..10969dc076 100644 --- a/app/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/app/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,7 +1,7 @@ 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 { useTranslation } from "react-i18next"; import styles from "./QuestionInput.module.css"; @@ -16,9 +16,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); const { t } = useTranslation(); @@ -87,9 +89,20 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend, init onCompositionEnd={handleCompositionEnd} />
- -
{showSpeechInput && } diff --git a/app/frontend/src/locales/en/translation.json b/app/frontend/src/locales/en/translation.json index e9539cd2ce..98f78829e5 100644 --- a/app/frontend/src/locales/en/translation.json +++ b/app/frontend/src/locales/en/translation.json @@ -42,6 +42,7 @@ "tooltips": { "submitQuestion": "Submit question", + "stopStreaming": "Stop streaming", "askWithVoice": "Ask question with voice", "stopRecording": "Stop recording question", "showThoughtProcess": "Show thought process", diff --git a/app/frontend/src/locales/es/translation.json b/app/frontend/src/locales/es/translation.json index 20a0294f20..18866a89ab 100644 --- a/app/frontend/src/locales/es/translation.json +++ b/app/frontend/src/locales/es/translation.json @@ -42,6 +42,7 @@ "tooltips": { "submitQuestion": "Enviar pregunta", + "stopStreaming": "Detener la transmisión", "askWithVoice": "Realizar pregunta con voz", "stopRecording": "Detener la grabación de la pregunta", "showThoughtProcess": "Mostrar proceso de pensamiento", diff --git a/app/frontend/src/locales/fr/translation.json b/app/frontend/src/locales/fr/translation.json index d58812f99e..c2d6669834 100644 --- a/app/frontend/src/locales/fr/translation.json +++ b/app/frontend/src/locales/fr/translation.json @@ -42,6 +42,7 @@ "tooltips": { "submitQuestion": "Soumettre une question", + "stopStreaming": "Arrêter la diffusion", "askWithVoice": "Poser une question à l'aide de la voix", "stopRecording": "Arrêter l'enregistrement de la question", "showThoughtProcess": "Montrer le processus de réflexion", diff --git a/app/frontend/src/locales/ja/translation.json b/app/frontend/src/locales/ja/translation.json index 7d0fc61cc5..9799e9a06d 100644 --- a/app/frontend/src/locales/ja/translation.json +++ b/app/frontend/src/locales/ja/translation.json @@ -42,6 +42,7 @@ "tooltips":{ "submitQuestion": "質問を送信", + "stopStreaming": "ストリーミングを停止", "askWithVoice": "音声で質問", "stopRecording": "質問の記録を停止", "showThoughtProcess": "思考プロセスの表示", diff --git a/app/frontend/src/pages/ask/Ask.tsx b/app/frontend/src/pages/ask/Ask.tsx index d91293267d..496f8f78b8 100644 --- a/app/frontend/src/pages/ask/Ask.tsx +++ b/app/frontend/src/pages/ask/Ask.tsx @@ -259,6 +259,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 001a9b8712..88dd774830 100644 --- a/app/frontend/src/pages/chat/Chat.tsx +++ b/app/frontend/src/pages/chat/Chat.tsx @@ -62,6 +62,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(); @@ -108,7 +110,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; @@ -116,6 +118,7 @@ const Chat = () => { return new Promise(resolve => { setTimeout(() => { answer += newContent; + setPartialResponse(answer); const latestResponse: ChatAppResponse = { ...askResponse, message: { content: answer, role: askResponse.message.role } @@ -128,6 +131,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; @@ -141,8 +147,11 @@ const Chat = () => { throw Error(event["error"]); } } + } catch (e) { + console.error("error in handleAsyncRequest: ", e); } finally { setIsStreaming(false); + setPartialResponse(""); } const fullResponse: ChatAppResponse = { ...askResponse, @@ -155,6 +164,8 @@ const Chat = () => { const { loggedIn } = useContext(LoginContext); const makeApiRequest = async (question: string) => { + const controller = new AbortController(); + setAbortController(controller); lastQuestionRef.current = question; error && setError(undefined); @@ -197,7 +208,7 @@ 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"); } @@ -205,7 +216,7 @@ const Chat = () => { throw Error(`Request failed with status ${response.status}`); } 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(); @@ -232,6 +243,7 @@ const Chat = () => { setStreamedAnswers([]); setIsLoading(false); setIsStreaming(false); + setPartialResponse(""); }; useEffect(() => chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }), [isLoading]); @@ -317,6 +329,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"); @@ -393,6 +415,33 @@ const Chat = () => { ))} + {partialResponse && !isStreaming && ( +
+ +
+ {}} + onThoughtProcessClicked={() => {}} + onSupportingContentClicked={() => {}} + onFollowupQuestionClicked={() => {}} + showFollowupQuestions={false} + speechUrl={null} + /> +
+
+ )} {!isStreaming && answers.map((answer, index) => (
@@ -443,6 +492,8 @@ const Chat = () => { disabled={isLoading} onSend={question => makeApiRequest(question)} showSpeechInput={showSpeechInput} + isStreaming={isStreaming} + onStop={onStopClick} />