Feat: If the interval between two outputs exceeds 600ms, a loading state is displayed at the end. (#16861)

### Summary

Feat: If the interval between two outputs exceeds 600ms, a loading state
is displayed at the end.
This commit is contained in:
balibabu
2026-07-13 18:02:29 +08:00
committed by GitHub
parent 8bc18154d2
commit 45862bf95d
7 changed files with 99 additions and 11 deletions

View File

@@ -0,0 +1,39 @@
.dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: currentColor;
animation: loadingDots 1.4s ease-in-out infinite both;
margin-right: 4px;
&:last-child {
margin-right: 0;
}
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0s;
}
}
@keyframes loadingDots {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}

View File

@@ -0,0 +1,15 @@
import styles from './index.module.less';
interface LoadingDotsProps {
className?: string;
}
export function LoadingDots({ className }: LoadingDotsProps) {
return (
<span className={className} aria-label="Loading">
<span className={styles.dot} />
<span className={styles.dot} />
<span className={styles.dot} />
</span>
);
}

View File

@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { useFetchDocumentThumbnailsByIds } from '@/hooks/use-document-request';
import { useLoadingPause } from '@/hooks/use-loading-pause';
import {
currentReg,
parseCitationIndex,
@@ -30,6 +31,7 @@ import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
import reactStringReplace from 'react-string-replace';
import { LoadingDots } from '../loading-dots';
import { Button } from '../ui/button';
import {
HoverCard,
@@ -52,6 +54,7 @@ const MarkdownContent = ({
reference,
clickDocumentButton,
content,
loading,
}: {
content: string;
loading: boolean;
@@ -260,6 +263,7 @@ const MarkdownContent = ({
);
const dir = getDirAttribute(content.replace(citationMarkerReg, ''));
const showLoadingDots = useLoadingPause(loading, content);
return (
<div dir={dir} className={styles.markdownContentWrapper}>
@@ -298,6 +302,9 @@ const MarkdownContent = ({
>
{contentWithCursor}
</Markdown>
{showLoadingDots && (
<LoadingDots className="ml-1 inline-block text-text-secondary" />
)}
</div>
);
};

View File

@@ -1,18 +1,17 @@
import { MessageType } from '@/constants/chat';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import {
IMessage,
IReference,
IReferenceChunk,
UploadResponseDataType,
} from '@/interfaces/database/chat';
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { cn } from '@/lib/utils';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { memo, useCallback, useMemo } from 'react';
import { DocumentDownloadButton } from '../document-download-button';
import { LoadingDots } from '../loading-dots';
import MarkdownContent from '../markdown-content';
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
import { ReferenceImageList } from '../next-message-item/reference-image-list';
@@ -54,7 +53,6 @@ const MessageItem = ({
visibleAvatar = true,
nickname,
}: IProps) => {
const { t } = useTranslation();
const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant;
const isUser = item.role === MessageType.User;
@@ -151,7 +149,7 @@ const MessageItem = ({
)}
>
{sendLoading && isEmpty(messageContent) ? (
t('common.running')
<LoadingDots className="text-text-secondary" />
) : (
<MarkdownContent
loading={loading}

View File

@@ -29,11 +29,13 @@ import { citationMarkerReg } from '@/utils/citation-utils';
import { getDirAttribute } from '@/utils/text-direction';
import { useFetchDocumentThumbnailsByIds } from '@/hooks/use-document-request';
import { useLoadingPause } from '@/hooks/use-loading-pause';
import { cn } from '@/lib/utils';
import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
import reactStringReplace from 'react-string-replace';
import { LoadingDots } from '../loading-dots';
import { Button } from '../ui/button';
import {
HoverCard,
@@ -159,6 +161,7 @@ function MarkdownContent({
reference,
clickDocumentButton,
content,
loading,
}: {
content: string;
loading: boolean;
@@ -353,6 +356,7 @@ function MarkdownContent({
);
const dir = getDirAttribute(content.replace(citationMarkerReg, ''));
const showLoadingDots = useLoadingPause(loading, content);
return (
<div dir={dir} className={styles.markdownContentWrapper}>
@@ -435,6 +439,9 @@ function MarkdownContent({
>
{contentWithCursor}
</Markdown>
{showLoadingDots && (
<LoadingDots className="ml-1 inline-block text-text-secondary" />
)}
</div>
);
}

View File

@@ -15,7 +15,6 @@ import {
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { INodeEvent, MessageEventType } from '@/hooks/use-send-message';
@@ -27,6 +26,7 @@ import { getDirAttribute } from '@/utils/text-direction';
import { isEmpty } from 'lodash';
import { Atom, ChevronDown, ChevronUp } from 'lucide-react';
import { DocumentDownloadButton } from '../document-download-button';
import { LoadingDots } from '../loading-dots';
import MarkdownContent from '../next-markdown-content';
import { RAGFlowAvatar } from '../ragflow-avatar';
import SvgIcon from '../svg-icon';
@@ -82,7 +82,6 @@ function MessageItem({
isShare,
nickname,
}: IProps) {
const { t } = useTranslation();
const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant;
const isUser = item.role === MessageType.User;
@@ -147,7 +146,7 @@ function MessageItem({
{hasCustomChildren ? (
children
) : sendLoading && isEmpty(messageContent) ? (
<>{!isShare && t('common.running')}</>
<>{!isShare && <LoadingDots className="text-text-secondary" />}</>
) : (
<MarkdownContent
loading={loading}
@@ -168,7 +167,6 @@ function MessageItem({
messageContent,
reference,
sendLoading,
t,
theme,
]);

View File

@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
export function useLoadingPause(
loading: boolean,
content: string,
delay = 600,
) {
const [show, setShow] = useState(false);
useEffect(() => {
if (!loading || !content) {
setShow(false);
return;
}
const timer = setTimeout(() => {
setShow(true);
}, delay);
return () => clearTimeout(timer);
}, [loading, content, delay]);
return show;
}