feat(canvas): show loading state while agent history loads

Track agent history loading in the workspace and render a spinner with a
localized "loading conversation history" message in the assistant feed,
so the empty state no longer flashes before history resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:42:41 +08:00
parent 0a68551bfd
commit 6f998480bc
4 changed files with 38 additions and 2 deletions
+1
View File
@@ -341,6 +341,7 @@
"conversation": "Chat", "conversation": "Chat",
"newChat": "New chat", "newChat": "New chat",
"conversationHistory": "History", "conversationHistory": "History",
"loadingConversationHistory": "Loading conversation history...",
"scrollToLatest": "Scroll to latest", "scrollToLatest": "Scroll to latest",
"searchConversationPlaceholder": "Search conversations", "searchConversationPlaceholder": "Search conversations",
"deleteConversation": "Delete conversation", "deleteConversation": "Delete conversation",
+1
View File
@@ -341,6 +341,7 @@
"conversation": "对话", "conversation": "对话",
"newChat": "新对话", "newChat": "新对话",
"conversationHistory": "历史对话", "conversationHistory": "历史对话",
"loadingConversationHistory": "正在加载历史对话...",
"scrollToLatest": "回到底部", "scrollToLatest": "回到底部",
"searchConversationPlaceholder": "请输入搜索关键词", "searchConversationPlaceholder": "请输入搜索关键词",
"deleteConversation": "删除对话", "deleteConversation": "删除对话",
@@ -575,6 +575,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
const { project, setProject, projectRef, viewport, setViewport, viewportRef, nodes, setNodes, nodesRef, applyProjectDocument } = useCanvasDocument(); const { project, setProject, projectRef, viewport, setViewport, viewportRef, nodes, setNodes, nodesRef, applyProjectDocument } = useCanvasDocument();
const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes); const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes);
const [messages, setMessages] = useState<AgentMessage[]>([]); const [messages, setMessages] = useState<AgentMessage[]>([]);
const [agentHistoryLoading, setAgentHistoryLoading] = useState(canViewChat);
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const [agentStarterConfig, setAgentStarterConfig] = useState<AgentStarterConfig>(() => defaultAgentStarterConfig(t)); const [agentStarterConfig, setAgentStarterConfig] = useState<AgentStarterConfig>(() => defaultAgentStarterConfig(t));
const [agentThreads, setAgentThreads] = useState<AgentThreadSummary[]>([]); const [agentThreads, setAgentThreads] = useState<AgentThreadSummary[]>([]);
@@ -901,17 +902,20 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
useEffect(() => { useEffect(() => {
selectedAgentThreadIdRef.current = null; selectedAgentThreadIdRef.current = null;
setSelectedAgentThreadId(null); setSelectedAgentThreadId(null);
setAgentHistoryLoading(canViewChat);
setAgentThreads([]); setAgentThreads([]);
setAgentHistorySearch(""); setAgentHistorySearch("");
setAgentHistoryOpen(false); setAgentHistoryOpen(false);
autoScrollAgentFeedRef.current = false; autoScrollAgentFeedRef.current = false;
}, [projectId]); }, [canViewChat, projectId]);
useEffect(() => { useEffect(() => {
if (agentHistoryOpen) void loadAgentThreads(); if (agentHistoryOpen) void loadAgentThreads();
}, [agentHistoryOpen, loadAgentThreads]); }, [agentHistoryOpen, loadAgentThreads]);
useEffect(() => { useEffect(() => {
let active = true;
setAgentHistoryLoading(canViewChat);
designGateway designGateway
.getProject(projectId) .getProject(projectId)
.then(({ project: nextProject }) => { .then(({ project: nextProject }) => {
@@ -961,7 +965,13 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
return; return;
} }
setError(toUserMessageText(err, "generateError")); setError(toUserMessageText(err, "generateError"));
})
.finally(() => {
if (active) setAgentHistoryLoading(false);
}); });
return () => {
active = false;
};
}, [applyProject, canViewChat, projectId, startProjectEvents]); }, [applyProject, canViewChat, projectId, startProjectEvents]);
useEffect(() => { useEffect(() => {
@@ -2649,6 +2659,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
autoScrollAgentFeedRef.current = false; autoScrollAgentFeedRef.current = false;
selectedAgentThreadIdRef.current = newAgentConversationId; selectedAgentThreadIdRef.current = newAgentConversationId;
setSelectedAgentThreadId(newAgentConversationId); setSelectedAgentThreadId(newAgentConversationId);
setAgentHistoryLoading(false);
setAgentHistoryOpen(false); setAgentHistoryOpen(false);
setPrompt(""); setPrompt("");
setMessages([]); setMessages([]);
@@ -4124,7 +4135,12 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
</div> </div>
<div className="assistant-feed" ref={assistantFeedRef} onScroll={updateAssistantScrollButton}> <div className="assistant-feed" ref={assistantFeedRef} onScroll={updateAssistantScrollButton}>
{!hasCurrentConversation && !agentTimelineActive ? ( {agentHistoryLoading ? (
<div className="assistant-history-loading" role="status" aria-live="polite" data-testid="agent-history-loading">
<Loader2 className="spin" size={20} aria-hidden="true" />
<span>{t("loadingConversationHistory")}</span>
</div>
) : !hasCurrentConversation && !agentTimelineActive ? (
<CanvasAgentEmptyState config={agentStarterConfig} disabled={!canEdit} onSelectPrompt={selectAgentStarterPrompt} /> <CanvasAgentEmptyState config={agentStarterConfig} disabled={!canEdit} onSelectPrompt={selectAgentStarterPrompt} />
) : ( ) : (
<CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} /> <CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} />
+18
View File
@@ -3789,6 +3789,24 @@ button:disabled {
background: #dfe0e2; background: #dfe0e2;
} }
.assistant-history-loading {
width: 100%;
min-height: 100%;
flex: 1 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #8a9099;
font-size: 13px;
line-height: 20px;
}
.assistant-history-loading svg {
color: #646a73;
}
.assistant-scroll-to-end { .assistant-scroll-to-end {
position: absolute; position: absolute;
inset-inline: 0; inset-inline: 0;