Skip to content
Snippets Groups Projects
Commit 8a242b6d authored by Andréas Livet's avatar Andréas Livet
Browse files

Various chat improvements

Avoid loosing chat state when switching tabs

Handle response abort
parent a955e858
No related branches found
No related tags found
No related merge requests found
......@@ -15,6 +15,7 @@ import AlertContext from "@/shared/contexts/AlertContext";
import Markdown from "react-markdown";
import { capitalize } from "lodash";
import { Link } from "react-router-dom";
import { CustomAbortDOMException } from "@/chat/exceptions/CustomAbortDOMException";
type ChatResponse = {
query: string;
......@@ -42,19 +43,21 @@ export const SimpleChat = () => {
const [currentChatResponse, setCurrentChatResponse] =
useState<ChatResponse | null>(null);
const [inputValue, setInputValue] = useState("");
// We'll use this controller to cancel the user prompt request
const [chatResponseController, setChatResponseController] =
useState<AbortController | null>(null);
const currentQueryRef = useRef<ChatQuery | null>(null);
const handleCancelSubmission = () => {
// formSubmitController?.abort(
// new CustomAbortDOMException(
// "La génération de réponse a été interrompue.",
// "Arrêt",
// "info",
// "formSubmitCancelButton"
// )
// );
// Fixme: maybe send negative feedback to API when the request is aborted
chatResponseController?.abort(
new CustomAbortDOMException(
"La génération de réponse a été interrompue.",
"Arrêt",
"info",
"formSubmitCancelButton"
)
);
};
const createQuery = async (
......@@ -92,9 +95,6 @@ export const SimpleChat = () => {
};
const handleSubmitForm = async () => {
// Right after form submission, we reset the message field
// setValue("query", "");
// We resize the message input to its original size
// if (inputMessageRef.current) {
// inputMessageRef.current.style.height = "auto";
......@@ -120,6 +120,9 @@ export const SimpleChat = () => {
try {
setIsChatRequestRunning(true);
// We'll use the AbortController to cancel the response if neede
const abortController = new AbortController();
setChatResponseController(abortController);
// We can now send the complete user prompt request to our API
const response = await fetch(
forgeUrl(`/chat/query/${currentQueryRef.current.id}/message`),
......@@ -129,6 +132,7 @@ export const SimpleChat = () => {
query: chatResponse.query,
collection_ids: [currentCollection!.id],
}),
signal: abortController.signal,
// signal: formSubmitAbortController.signal,
headers: {
Accept: "text/event-stream",
......@@ -161,7 +165,7 @@ export const SimpleChat = () => {
setIsSubmitting(false);
const query = currentQueryRef.current;
if (query) {
query.history = currentChatResponse;
query.history = [currentChatResponse];
fetch(forgeUrl(`/chat/query/${query.id}`), {
headers: {
Authorization: accessToken!,
......@@ -388,7 +392,6 @@ export const SimpleChat = () => {
{capitalize(currentChatResponse.errorMessage)}
</div>
)}
{currentChatResponse?.errorMessage}
</>
)}
</div>
......@@ -400,80 +403,95 @@ export const SimpleChat = () => {
</div>
{/* Message input */}
<div className={classes.inputContainer}>
{/* Message input */}
<div>
<Input
label={null}
disabled={
!(
isProcessingServiceAvailable && isDocumentsProcessingStatusOk
) || isSubmitting
}
textArea
className={cx(classes.input)}
// state={errors?.message && 'error'}
// stateRelatedMessage={errors?.message?.message}
nativeTextAreaProps={{
placeholder: getPromptPlaceholderText(),
rows: 1,
// onChange: (e) => {
// if (inputMessageRef.current) {
// inputMessageRef.current.style.height = "auto";
// inputMessageRef.current.style.height = `${e.target.scrollHeight}px`;
// }
// },
onChange: (e) => {
setInputValue(e.target.value);
},
onKeyPress: (e) => {
// We'll submit the form when pressing 'Enter' without the 'Shift' key
if (e.key === "Enter" && !e.shiftKey) {
setIsSubmitting(!isSubmitting);
handleOnSubmit();
{!currentChatResponse || isSubmitting ? (
<>
{/* Message input */}
<div>
<Input
label={null}
disabled={
!(
isProcessingServiceAvailable &&
isDocumentsProcessingStatusOk
) || isSubmitting
}
},
value: inputValue,
}}
/>
</div>
textArea
className={cx(classes.input)}
// state={errors?.message && 'error'}
// stateRelatedMessage={errors?.message?.message}
nativeTextAreaProps={{
placeholder: getPromptPlaceholderText(),
rows: 1,
// onChange: (e) => {
// if (inputMessageRef.current) {
// inputMessageRef.current.style.height = "auto";
// inputMessageRef.current.style.height = `${e.target.scrollHeight}px`;
// }
// },
onChange: (e) => {
setInputValue(e.target.value);
},
onKeyPress: (e) => {
// We'll submit the form when pressing 'Enter' without the 'Shift' key
if (e.key === "Enter" && !e.shiftKey) {
setIsSubmitting(!isSubmitting);
handleOnSubmit();
}
},
value: inputValue,
}}
/>
</div>
{/* Send message button */}
<div
className={cx(
classes.inputButtonContainer,
classes.sendMessageButtonContainer
)}
>
{/* Send message loading spinner */}
<ActionButton
isLoading={isSubmitting}
sx={{
color: fr.colors.decisions.background.default.grey.default,
}}
/>
{/* Send message icon button */}
<Button
iconId={
isSubmitting
? "fr-icon-stop-circle-fill"
: "fr-icon-send-plane-fill"
}
disabled={
!isProcessingServiceAvailable || !isDocumentsProcessingStatusOk
}
className={cx(classes.inputIcon, classes.sendMessageIcon)}
title={
isSubmitting
? "Arrêt de l'envoi du message"
: "Envoi du message"
}
onClick={() => {
setIsSubmitting(!isSubmitting);
handleOnSubmit();
}}
/>
</div>
{/* Send message button */}
<div
className={cx(
classes.inputButtonContainer,
classes.sendMessageButtonContainer
)}
>
{/* Send message loading spinner */}
<ActionButton
isLoading={isSubmitting}
sx={{
color: fr.colors.decisions.background.default.grey.default,
}}
/>
{/* Send message icon button */}
<Button
iconId={
isSubmitting
? "fr-icon-stop-circle-fill"
: "fr-icon-send-plane-fill"
}
disabled={
!isProcessingServiceAvailable ||
!isDocumentsProcessingStatusOk
}
className={cx(classes.inputIcon, classes.sendMessageIcon)}
title={
isSubmitting
? "Arrêt de l'envoi du message"
: "Envoi du message"
}
onClick={() => {
setIsSubmitting(!isSubmitting);
handleOnSubmit();
}}
/>
</div>
</>
) : (
<>
<Button
iconId="fr-icon-add-circle-line"
onClick={() => setCurrentChatResponse(null)}
>
Nouvelle conversation
</Button>
</>
)}
</div>
</div>
</div>
......
import React, { ReactElement, useState } from "react";
import React, { ReactElement, useEffect, useState } from "react";
import {
BrowserRouter as Router,
Route,
......@@ -27,6 +27,14 @@ export const TabComponent = ({ tabs }: { tabs: Tab[] }) => {
setActiveTabIndex(index);
};
// useEffect(() => {
// for (const [index, tab] of tabs.entries()) {
// if (tab.isActive && activeTabIndex !== index) {
// setActiveTabIndex(index);
// }
// }
// }, [tabs]);
return (
<div>
<nav className="fr-nav" role="navigation" data-fr-js-navigation="true">
......@@ -48,7 +56,7 @@ export const TabComponent = ({ tabs }: { tabs: Tab[] }) => {
handleTabClick(index);
}}
data-discover="true"
aria-current={tab.isActive && "page"}
aria-current={activeTabIndex === index && "page"}
>
{tab.text}
</a>
......@@ -56,10 +64,16 @@ export const TabComponent = ({ tabs }: { tabs: Tab[] }) => {
))}
</ul>
</nav>
<div className={styles.tabContent}>
{tabs[activeTabIndex].content}
</div>
{/* Avoid loosing React state of the components */}
{tabs.map((tab, index) => (
<div
key={index}
className={styles.tabContent}
style={{ display: activeTabIndex === index ? "block" : "none" }}
>
{tab.content}
</div>
))}
</div>
);
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment