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; +}