输入框状态剥离,避免受长信息列表渲染影响;覆写滚动条状态动作,不再强制拉到最底

This commit is contained in:
2026-06-03 15:01:24 +08:00
parent 888132a60f
commit fa3e6b6e84
3 changed files with 90 additions and 47 deletions
+43 -30
View File
@@ -10,7 +10,7 @@ import { Box, Drawer, alpha, useTheme } from "@mui/material";
import type { AgentModel } from "@/lib/chatStream";
import { useProjectStore } from "@/store/projectStore";
import { AgentComposer } from "./AgentComposer";
import { AgentComposer, type AgentComposerHandle } from "./AgentComposer";
import { AgentHeader } from "./AgentHeader";
import { AgentHistoryPanel } from "./AgentHistoryPanel";
import { AgentWorkspace } from "./AgentWorkspace";
@@ -22,7 +22,6 @@ import { useAgentChatSession } from "./hooks/useAgentChatSession";
import { useAgentToolActions } from "./hooks/useAgentToolActions";
export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const [input, setInput] = useState("");
const [width, setWidth] = useState(520);
const [isResizing, setIsResizing] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
@@ -31,8 +30,10 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
);
const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const workspaceRef = useRef<HTMLDivElement | null>(null);
const composerRef = useRef<AgentComposerHandle | null>(null);
const hasResetForOpenRef = useRef(false);
const shouldAutoScrollRef = useRef(true);
const theme = useTheme();
const currentProjectId = useProjectStore((state) => state.currentProjectId);
@@ -47,7 +48,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
} = useSpeechSynthesis();
const handleSpeechResult = useCallback((text: string) => {
setInput((prev) => prev + text);
composerRef.current?.append(text);
}, []);
const {
@@ -83,9 +84,22 @@ export const GlobalChatbox: React.FC<Props> = ({ 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(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isStreaming]);
if (!shouldAutoScrollRef.current) return;
scrollToBottom(isStreaming ? "auto" : "smooth");
}, [isStreaming, messages, scrollToBottom]);
useEffect(() => {
if (!open) {
@@ -96,38 +110,33 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
hasResetForOpenRef.current = true;
const timer = window.setTimeout(() => {
shouldAutoScrollRef.current = true;
createSession();
setInput("");
composerRef.current?.clear();
setIsHistoryOpen(false);
inputRef.current?.focus();
bottomRef.current?.scrollIntoView({ behavior: "auto" });
composerRef.current?.focus();
scrollToBottom("auto");
}, 0);
return () => window.clearTimeout(timer);
}, [createSession, isHydrating, open]);
}, [createSession, isHydrating, open, scrollToBottom]);
const handleSend = useCallback(() => {
const prompt = input.trim();
if (!prompt || isStreaming) return;
setInput("");
const handleSend = useCallback((prompt: string) => {
if (isStreaming) return;
shouldAutoScrollRef.current = true;
void sendPrompt(prompt);
}, [input, isStreaming, sendPrompt]);
const handlePresetPromptSelect = useCallback((prompt: string) => {
setInput(prompt);
window.setTimeout(() => {
inputRef.current?.focus();
}, 0);
}, []);
}, [isStreaming, sendPrompt]);
const handleNewConversation = useCallback(() => {
handleStopSpeech();
stopListening();
shouldAutoScrollRef.current = true;
createSession();
setInput("");
composerRef.current?.clear();
window.setTimeout(() => {
inputRef.current?.focus();
composerRef.current?.focus();
scrollToBottom("auto");
}, 0);
}, [createSession, handleStopSpeech, stopListening]);
}, [createSession, handleStopSpeech, scrollToBottom, stopListening]);
const handleHistoryToggle = useCallback(() => {
setIsHistoryOpen((prev) => !prev);
@@ -135,12 +144,17 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const handleSelectSession = useCallback(
(storageSessionId: string) => {
setInput("");
shouldAutoScrollRef.current = true;
composerRef.current?.clear();
void switchSession(storageSessionId);
},
[switchSession],
);
const handleWorkspaceScroll = useCallback(() => {
syncAutoScrollState();
}, [syncAutoScrollState]);
const handleDeleteSession = useCallback(
(storageSessionId: string) => {
void removeSession(storageSessionId);
@@ -320,6 +334,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
branchGroups={branchGroups}
branchTransition={branchTransition}
isStreaming={isStreaming}
scrollContainerRef={workspaceRef}
onScroll={handleWorkspaceScroll}
bottomRef={bottomRef}
speakingMessageId={speakingMessageId}
speechState={speechState}
@@ -334,19 +350,16 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
/>
<AgentComposer
input={input}
inputRef={inputRef}
ref={composerRef}
isHydrating={isHydrating}
isStreaming={isStreaming}
isListening={isListening}
isSttSupported={isSttSupported}
presets={PRESET_PROMPTS}
onInputChange={setInput}
onSend={handleSend}
onAbort={abort}
onStartListening={startListening}
onStopListening={stopListening}
onPresetSelect={handlePresetPromptSelect}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
/>