(front) add retry button

on chat error add retry button
This commit is contained in:
Eléonore Voisin
2025-12-01 19:21:34 +01:00
parent cdac7cad3b
commit d3922b7448
3 changed files with 145 additions and 21 deletions
+4
View File
@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased]
### Added
- ✨(front) add retry button
### Fixed
- 🚑️(user) allow longer short names #182
@@ -17,7 +17,7 @@ import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Box, Icon, Loader, StyledLink, Text } from '@/components';
import { Box, Icon, Loader, Text } from '@/components';
import { useUploadFile } from '@/features/attachments/hooks/useUploadFile';
import { useChat } from '@/features/chat/api/useChat';
import { getConversation } from '@/features/chat/api/useConversation';
@@ -27,6 +27,7 @@ import {
useLLMConfiguration,
} from '@/features/chat/api/useLLMConfiguration';
import { AttachmentList } from '@/features/chat/components/AttachmentList';
import { ChatError } from '@/features/chat/components/ChatError';
import { CodeBlock } from '@/features/chat/components/CodeBlock';
import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons';
import { InputChat } from '@/features/chat/components/InputChat';
@@ -160,12 +161,21 @@ export const Chat = ({
forceWebSearch?: boolean;
} | null>(null);
const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false);
const [shouldRetry, setShouldRetry] = useState(false);
const retryOriginalInputRef = useRef<string>('');
const retryOriginalFilesRef = useRef<FileList | null>(null);
const [hasInitialized, setHasInitialized] = useState(false);
const [streamingMessageHeight, setStreamingMessageHeight] = useState<
number | null
>(null);
const lastUserMessageIdRef = useRef<string | null>(null);
const hasScrolledToBottomOnLoadRef = useRef(false);
const lastSubmissionRef = useRef<{
input: string;
files: FileList | null;
event: FormEvent<HTMLFormElement>;
options?: Record<string, unknown>;
} | null>(null);
const { mutate: createChatConversation } = useCreateChatConversation();
@@ -214,6 +224,7 @@ export const Chat = ({
handleInputChange,
status,
stop: stopChat,
setMessages,
} = useChat({
id: conversationId,
initialMessages: initialConversationMessages,
@@ -250,6 +261,29 @@ export const Chat = ({
void handleSubmit(event);
};
const handleRetry = () => {
if (!lastSubmissionRef.current || !setMessages) {
return;
}
const { input: lastInput, files: lastFiles } = lastSubmissionRef.current;
const lastAssistantIndex = messages.findLastIndex(
(msg) => msg.role === 'assistant',
);
if (lastAssistantIndex !== -1) {
setMessages(messages.filter((_, index) => index !== lastAssistantIndex));
}
retryOriginalInputRef.current = input;
retryOriginalFilesRef.current = files;
handleInputChange({
target: { value: lastInput },
} as ChangeEvent<HTMLTextAreaElement>);
setFiles(lastFiles);
setShouldRetry(true);
};
// Précharger les métadonnées des sources dès que les messages arrivent
useEffect(() => {
messages.forEach((message) => {
@@ -406,6 +440,25 @@ export const Chat = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldAutoSubmit, input, files]);
useEffect(() => {
if (
shouldRetry &&
lastSubmissionRef.current &&
input === lastSubmissionRef.current.input
) {
const { event } = lastSubmissionRef.current;
void handleSubmit(event);
handleInputChange({
target: { value: retryOriginalInputRef.current },
} as ChangeEvent<HTMLTextAreaElement>);
setFiles(retryOriginalFilesRef.current);
setShouldRetry(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldRetry, input, files]);
// Fetch initial conversation messages if initialConversationId is provided and no pending input
useEffect(() => {
hasScrolledToBottomOnLoadRef.current = false; // Réinitialiser au début du chargement
@@ -538,6 +591,13 @@ export const Chat = ({
options.experimental_attachments = attachments;
}
lastSubmissionRef.current = {
input,
files,
event,
options: Object.keys(options).length > 0 ? options : undefined,
};
if (Object.keys(options).length > 0) {
baseHandleSubmit(event, options);
} else {
@@ -920,26 +980,10 @@ export const Chat = ({
</Box>
) : null}
{status === 'error' && (
<Box
$direction={isMobile ? 'column' : 'row'}
$gap="6px"
$width="100%"
$maxWidth="750px"
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
$padding={{ left: '13px' }}
>
<Text>{t('Sorry, an error occurred. Please try again.')}</Text>
<StyledLink
href="/"
rel="noopener noreferrer"
$css={`
color: var(--c--theme--colors--greyscale-900);
text-decoration: underline;
`}
>
{t('Start a new conversation.')}
</StyledLink>
</Box>
<ChatError
hasLastSubmission={!!lastSubmissionRef.current}
onRetry={handleRetry}
/>
)}
</Box>
<Box
@@ -0,0 +1,76 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { Box, Icon, Text } from '@/components';
interface ChatErrorProps {
hasLastSubmission: boolean;
onRetry: () => void;
}
export const ChatError = ({ hasLastSubmission, onRetry }: ChatErrorProps) => {
const { t } = useTranslation();
const router = useRouter();
return (
<Box
$direction="column"
$gap="6px"
$width="100%"
$maxWidth="750px"
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
$padding={{ left: '13px' }}
>
<Text $variation="550" $theme="greyscale">
{t('Sorry, an error occurred. Please try again.')}
</Text>
<Box
$direction="row"
$gap="6px"
$align="center"
$margin={{ top: '10px' }}
>
{hasLastSubmission ? (
<Button
size="small"
color="tertiary"
onClick={onRetry}
className="retry-button"
style={{
color: 'var(--c--theme--colors--greyscale-550)',
borderColor: 'var(--c--theme--colors--greyscale-300)',
}}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="15"
viewBox="0 0 11 15"
fill="none"
>
<path
d="M0.733333 10.0333C0.488889 9.61111 0.305556 9.17778 0.183333 8.73333C0.0611111 8.28889 0 7.83333 0 7.36667C0 5.87778 0.516667 4.61111 1.55 3.56667C2.58333 2.52222 3.84444 2 5.33333 2H5.45L4.38333 0.933333L5.31667 0L7.98333 2.66667L5.31667 5.33333L4.38333 4.4L5.45 3.33333H5.33333C4.22222 3.33333 3.27778 3.725 2.5 4.50833C1.72222 5.29167 1.33333 6.24444 1.33333 7.36667C1.33333 7.65556 1.36667 7.93889 1.43333 8.21667C1.5 8.49444 1.6 8.76667 1.73333 9.03333L0.733333 10.0333ZM5.35 14.6667L2.68333 12L5.35 9.33333L6.28333 10.2667L5.21667 11.3333H5.33333C6.44444 11.3333 7.38889 10.9417 8.16667 10.1583C8.94444 9.375 9.33333 8.42222 9.33333 7.3C9.33333 7.01111 9.3 6.72778 9.23333 6.45C9.16667 6.17222 9.06667 5.9 8.93333 5.63333L9.93333 4.63333C10.1778 5.05556 10.3611 5.48889 10.4833 5.93333C10.6056 6.37778 10.6667 6.83333 10.6667 7.3C10.6667 8.78889 10.15 10.0556 9.11667 11.1C8.08333 12.1444 6.82222 12.6667 5.33333 12.6667H5.21667L6.28333 13.7333L5.35 14.6667Z"
fill="currentColor"
/>
</svg>
}
>
{t('Retry')}
</Button>
) : (
<Button
size="small"
color="primary"
onClick={() => {
void router.push('/');
}}
icon={<Icon iconName="add" $color="white" />}
>
{t('Start a new conversation')}
</Button>
)}
</Box>
</Box>
);
};