diff --git a/src/components/chat/AgentProgressTimeline.tsx b/src/components/chat/AgentProgressTimeline.tsx index cecf6fd..83c99e6 100644 --- a/src/components/chat/AgentProgressTimeline.tsx +++ b/src/components/chat/AgentProgressTimeline.tsx @@ -85,7 +85,12 @@ const formatToolTitle = (item: ChatProgress) => { return item.title; }; -export const AgentProgressTimeline = ({ progress, isAborted }: { progress: ChatProgress[], isAborted?: boolean }) => { +type AgentProgressTimelineProps = { + progress: ChatProgress[]; + isAborted?: boolean; +}; + +const AgentProgressTimelineInner = ({ progress, isAborted }: AgentProgressTimelineProps) => { const theme = useTheme(); const [nowMs, setNowMs] = useState(() => Date.now()); @@ -356,3 +361,12 @@ export const AgentProgressTimeline = ({ progress, isAborted }: { progress: ChatP ); }; + +export const AgentProgressTimeline = React.memo( + AgentProgressTimelineInner, + (prevProps, nextProps) => + prevProps.progress === nextProps.progress && + prevProps.isAborted === nextProps.isAborted, +); + +AgentProgressTimeline.displayName = "AgentProgressTimeline"; diff --git a/src/components/chat/AgentTurn.tsx b/src/components/chat/AgentTurn.tsx index 8083954..03de6ce 100644 --- a/src/components/chat/AgentTurn.tsx +++ b/src/components/chat/AgentTurn.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import React from "react"; +import React, { useMemo } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { AnimatePresence, motion } from "framer-motion"; @@ -85,15 +85,21 @@ export const AgentTurn = React.memo( const [editDraft, setEditDraft] = React.useState(message.content); const rootMessageId = message.branchRootId ?? message.id; - const parsedAssistantSections = - !isUser && !isErrorMessage - ? parseAssistantMessageSections(message.content) - : null; + const parsedAssistantSections = useMemo( + () => + !isUser && !isErrorMessage + ? parseAssistantMessageSections(message.content) + : null, + [isErrorMessage, isUser, message.content], + ); const answerContent = parsedAssistantSections?.answer ?? message.content; - const contentSegments: ContentSegment[] = - !isUser && !isErrorMessage - ? parseContentWithToolCalls(answerContent).segments - : [{ type: "text", content: answerContent }]; + const contentSegments: ContentSegment[] = useMemo( + () => + !isUser && !isErrorMessage + ? parseContentWithToolCalls(answerContent).segments + : [{ type: "text", content: answerContent }], + [answerContent, isErrorMessage, isUser], + ); if (isUser) { return ( diff --git a/src/components/chat/AgentWorkspace.tsx b/src/components/chat/AgentWorkspace.tsx index c85b387..06eafb9 100644 --- a/src/components/chat/AgentWorkspace.tsx +++ b/src/components/chat/AgentWorkspace.tsx @@ -23,8 +23,6 @@ type AgentWorkspaceProps = { branchGroups: BranchGroup[]; branchTransition: BranchTransition | null; isStreaming: boolean; - scrollContainerRef: React.RefObject; - onScroll: React.UIEventHandler; bottomRef: React.RefObject; speakingMessageId: string | null; speechState: SpeechState; @@ -157,8 +155,6 @@ export const AgentWorkspace = ({ branchGroups, branchTransition, isStreaming, - scrollContainerRef, - onScroll, bottomRef, speakingMessageId, speechState, @@ -220,8 +216,6 @@ export const AgentWorkspace = ({ return ( = ({ open, onClose }) => { ); const bottomRef = useRef(null); - const workspaceRef = useRef(null); const composerRef = useRef(null); const hasResetForOpenRef = useRef(false); - const shouldAutoScrollRef = useRef(true); const theme = useTheme(); const currentProjectId = useProjectStore((state) => state.currentProjectId); @@ -84,20 +82,11 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { getModel: () => selectedModel, }); - const syncAutoScrollState = useCallback(() => { - const container = workspaceRef.current; - if (!container) return; - const distanceFromBottom = - container.scrollHeight - container.scrollTop - container.clientHeight; - shouldAutoScrollRef.current = distanceFromBottom <= 120; - }, []); - const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { bottomRef.current?.scrollIntoView({ behavior }); }, []); useEffect(() => { - if (!shouldAutoScrollRef.current) return; scrollToBottom(isStreaming ? "auto" : "smooth"); }, [isStreaming, messages, scrollToBottom]); @@ -110,7 +99,6 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { hasResetForOpenRef.current = true; const timer = window.setTimeout(() => { - shouldAutoScrollRef.current = true; createSession(); composerRef.current?.clear(); setIsHistoryOpen(false); @@ -122,14 +110,12 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const handleSend = useCallback((prompt: string) => { if (isStreaming) return; - shouldAutoScrollRef.current = true; void sendPrompt(prompt); }, [isStreaming, sendPrompt]); const handleNewConversation = useCallback(() => { handleStopSpeech(); stopListening(); - shouldAutoScrollRef.current = true; createSession(); composerRef.current?.clear(); window.setTimeout(() => { @@ -144,17 +130,12 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const handleSelectSession = useCallback( (storageSessionId: string) => { - shouldAutoScrollRef.current = true; composerRef.current?.clear(); void switchSession(storageSessionId); }, [switchSession], ); - const handleWorkspaceScroll = useCallback(() => { - syncAutoScrollState(); - }, [syncAutoScrollState]); - const handleDeleteSession = useCallback( (storageSessionId: string) => { void removeSession(storageSessionId); @@ -334,8 +315,6 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { branchGroups={branchGroups} branchTransition={branchTransition} isStreaming={isStreaming} - scrollContainerRef={workspaceRef} - onScroll={handleWorkspaceScroll} bottomRef={bottomRef} speakingMessageId={speakingMessageId} speechState={speechState}