💄 style: improve agent loading state (#11511)

* improve loading state

* improve loading state

* improve loading state
This commit is contained in:
Arvin Xu
2026-01-15 14:44:16 +08:00
committed by GitHub
parent 26ef2ff025
commit 3bb7f331f2
9 changed files with 87 additions and 20 deletions

View File

@@ -203,6 +203,8 @@
"noMembersYet": "This group doesn't have any members yet. Click the + button to invite agents.",
"noSelectedAgents": "No members selected yet",
"openInNewWindow": "Open in New Window",
"operation.execAgentRuntime": "Preparing response",
"operation.sendMessage": "Sending message",
"owner": "Group owner",
"pageCopilot.title": "Page Agent",
"pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and Ill refine the rest.",

View File

@@ -203,6 +203,8 @@
"noMembersYet": "这个群组还没有成员。点击「+」邀请助理加入",
"noSelectedAgents": "还未选择成员",
"openInNewWindow": "在新窗口打开",
"operation.execAgentRuntime": "准备响应中",
"operation.sendMessage": "消息发送中",
"owner": "群主",
"pageCopilot.title": "文稿助理",
"pageCopilot.welcome": "**让文字更清晰、更到位**\n\n起草、改写、润色都可以。你把意图说清楚其余交给我打磨",

View File

@@ -21,7 +21,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
chatInputEditorRef,
onMarkdownContentChange,
mentionItems,
allowExpand,
allowExpand = true,
}) => {
const editor = useEditor();
const slashMenuRef = useRef<HTMLDivElement>(null);

View File

@@ -8,7 +8,6 @@ import { CollapsedMessage } from '../../AssistantGroup/components/CollapsedMessa
import DisplayContent from '../../components/DisplayContent';
import FileChunks from '../../components/FileChunks';
import ImageFileListViewer from '../../components/ImageFileListViewer';
import IntentUnderstanding from '../../components/IntentUnderstanding';
import Reasoning from '../../components/Reasoning';
import SearchGrounding from '../../components/SearchGrounding';
import { useMarkdown } from '../useMarkdown';
@@ -23,9 +22,6 @@ const MessageContent = memo<UIChatMessage>(
const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
// TODO: Need to implement isIntentUnderstanding selector in ConversationStore if needed
const isIntentUnderstanding = false;
const showSearch = !!search && !!search.citations?.length;
const showImageItems = !!imageList && imageList.length > 0;
@@ -46,18 +42,15 @@ const MessageContent = memo<UIChatMessage>(
)}
{showFileChunks && <FileChunks data={chunksList} />}
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
{isIntentUnderstanding ? (
<IntentUnderstanding />
) : (
<DisplayContent
content={content}
hasImages={showImageItems}
isMultimodal={metadata?.isMultimodal}
isToolCallGenerating={isToolCallGenerating}
markdownProps={markdownProps}
tempDisplayContent={metadata?.tempDisplayContent}
/>
)}
<DisplayContent
content={content}
hasImages={showImageItems}
id={id}
isMultimodal={metadata?.isMultimodal}
isToolCallGenerating={isToolCallGenerating}
markdownProps={markdownProps}
tempDisplayContent={metadata?.tempDisplayContent}
/>
{showImageItems && <ImageFileListViewer items={imageList} />}
</Flexbox>
);

View File

@@ -52,6 +52,7 @@ const MessageContent = memo<UIChatMessage>(
<DisplayContent
content={content}
hasImages={showImageItems}
id={id}
isMultimodal={metadata?.isMultimodal}
isToolCallGenerating={isToolCallGenerating}
markdownProps={markdownProps}

View File

@@ -0,0 +1,64 @@
import { Flexbox, Text } from '@lobehub/ui';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import BubblesLoading from '@/components/BubblesLoading';
import { useChatStore } from '@/store/chat';
import { operationSelectors } from '@/store/chat/selectors';
import type { OperationType } from '@/store/chat/slices/operation/types';
const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
interface ContentLoadingProps {
id: string;
}
const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
const { t } = useTranslation('chat');
const operations = useChatStore(operationSelectors.getOperationsByMessage(id));
const [elapsedSeconds, setElapsedSeconds] = useState(0);
// Get the running operation
const runningOp = operations.find((op) => op.status === 'running');
const operationType = runningOp?.type as OperationType | undefined;
const startTime = runningOp?.metadata?.startTime;
// Track elapsed time, reset when operation type changes
useEffect(() => {
if (!startTime) {
setElapsedSeconds(0);
return;
}
const updateElapsed = () => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
setElapsedSeconds(elapsed);
};
updateElapsed();
const interval = setInterval(updateElapsed, 1000);
return () => clearInterval(interval);
}, [startTime, operationType]);
// Get localized label based on operation type
const operationLabel = operationType
? (t(`operation.${operationType}` as any) as string)
: undefined;
const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
return (
<Flexbox align={'center'} horizontal>
<BubblesLoading />
{operationLabel && (
<Text type={'secondary'}>
{operationLabel}...
{showElapsedTime && ` (${elapsedSeconds}s)`}
</Text>
)}
</Flexbox>
);
});
export default ContentLoading;

View File

@@ -2,17 +2,18 @@ import { deserializeParts } from '@lobechat/utils';
import { type MarkdownProps } from '@lobehub/ui';
import { memo } from 'react';
import BubblesLoading from '@/components/BubblesLoading';
import { LOADING_FLAT } from '@/const/message';
import MarkdownMessage from '@/features/Conversation/Markdown';
import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
import ContentLoading from './ContentLoading';
import { RichContentRenderer } from './RichContentRenderer';
const DisplayContent = memo<{
addIdOnDOM?: boolean;
content: string;
hasImages?: boolean;
id: string;
isMultimodal?: boolean;
isToolCallGenerating?: boolean;
markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
@@ -25,11 +26,12 @@ const DisplayContent = memo<{
hasImages,
isMultimodal,
tempDisplayContent,
id,
}) => {
const message = normalizeThinkTags(processWithArtifact(content));
if (isToolCallGenerating) return;
if ((!content && !hasImages) || content === LOADING_FLAT) return <BubblesLoading />;
if ((!content && !hasImages) || content === LOADING_FLAT) return <ContentLoading id={id} />;
const contentParts = isMultimodal ? deserializeParts(tempDisplayContent || content) : null;

View File

@@ -229,6 +229,8 @@ export default {
'noMembersYet': "This group doesn't have any members yet. Click the + button to invite agents.",
'noSelectedAgents': 'No members selected yet',
'openInNewWindow': 'Open in New Window',
'operation.execAgentRuntime': 'Preparing response',
'operation.sendMessage': 'Sending message',
'owner': 'Group owner',
'pageCopilot.title': 'Page Agent',
'pageCopilot.welcome':

View File

@@ -204,8 +204,9 @@ export const conversationLifecycle: StateCreator<
);
get().internal_toggleMessageLoading(true, tempId);
// Associate temp message with operation
// Associate temp messages with operation
get().associateMessageWithOperation(tempId, operationId);
get().associateMessageWithOperation(tempAssistantId, operationId);
// Store editor state in operation metadata for cancel restoration
const jsonState = mainInputEditor?.getJSONState();