diff --git a/web/src/components/loading-dots/index.module.less b/web/src/components/loading-dots/index.module.less
new file mode 100644
index 0000000000..e3e8906e14
--- /dev/null
+++ b/web/src/components/loading-dots/index.module.less
@@ -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);
+ }
+}
diff --git a/web/src/components/loading-dots/index.tsx b/web/src/components/loading-dots/index.tsx
new file mode 100644
index 0000000000..ad1e67cdbc
--- /dev/null
+++ b/web/src/components/loading-dots/index.tsx
@@ -0,0 +1,15 @@
+import styles from './index.module.less';
+
+interface LoadingDotsProps {
+ className?: string;
+}
+
+export function LoadingDots({ className }: LoadingDotsProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/web/src/components/markdown-content/index.tsx b/web/src/components/markdown-content/index.tsx
index f33fb745d7..7f3e4b21a2 100644
--- a/web/src/components/markdown-content/index.tsx
+++ b/web/src/components/markdown-content/index.tsx
@@ -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 (
@@ -298,6 +302,9 @@ const MarkdownContent = ({
>
{contentWithCursor}
+ {showLoadingDots && (
+
+ )}
);
};
diff --git a/web/src/components/message-item/index.tsx b/web/src/components/message-item/index.tsx
index de238dd848..5530fa0e0f 100644
--- a/web/src/components/message-item/index.tsx
+++ b/web/src/components/message-item/index.tsx
@@ -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')
+
) : (
@@ -435,6 +439,9 @@ function MarkdownContent({
>
{contentWithCursor}
+ {showLoadingDots && (
+
+ )}
);
}
diff --git a/web/src/components/next-message-item/index.tsx b/web/src/components/next-message-item/index.tsx
index 48db836ed9..9c5f3d5ce0 100644
--- a/web/src/components/next-message-item/index.tsx
+++ b/web/src/components/next-message-item/index.tsx
@@ -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 && }>
) : (
{
+ if (!loading || !content) {
+ setShow(false);
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setShow(true);
+ }, delay);
+
+ return () => clearTimeout(timer);
+ }, [loading, content, delay]);
+
+ return show;
+}