From 894eeb8c4b5bbc37521c33bdbe109b445fa2a2c6 Mon Sep 17 00:00:00 2001 From: alboped Date: Tue, 26 May 2026 17:08:24 +0800 Subject: [PATCH] =?UTF-8?q?perf(home):=20=E6=8B=86=E5=88=86=20ChatComposer?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E8=BE=93=E5=85=A5=E6=A1=86=E6=89=93?= =?UTF-8?q?=E5=AD=97=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将输入草稿状态从 HomePage 抽离为独立 memo 组件,避免每次按键重渲染消息列表与侧栏;并微调输入区与滚动条样式。 Co-authored-by: Cursor --- src/pages/index.tsx | 482 +++++++++++++++++++++++++------------------- 1 file changed, 275 insertions(+), 207 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d5dd681..9cdd91a 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,15 @@ import type { CSSProperties } from "react"; -import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from "react"; +import { + Atom, ArrowUp, ChevronDown, Copy, @@ -13,7 +22,7 @@ import { Pencil, Pin, RotateCcw, - Search, + Globe, Settings, Share2, Smartphone, @@ -120,6 +129,113 @@ function replaceSessionUrl(sessionId: string) { window.history.replaceState(window.history.state, "", nextPath); } +type ChatComposerProps = { + onSend: (text: string, clearDraft: () => void) => Promise; + isSending: boolean; + /** 正在创建首条会话时锁定,避免重复点击 */ + creationLocked: boolean; + deepThink: boolean; + onDeepThinkToggle: () => void; + smartSearch: boolean; + onSmartSearchToggle: () => void; + showScrollToBottom: boolean; + onScrollToBottomClick: () => void; +}; + +/** + * 独立维护输入草稿:按键时只重渲染本区域,避免整页(消息列表、侧栏)随输入重绘导致卡顿。 + */ +const ChatComposer = memo(function ChatComposer({ + onSend, + isSending, + creationLocked, + deepThink, + onDeepThinkToggle, + smartSearch, + onSmartSearchToggle, + showScrollToBottom, + onScrollToBottomClick, +}: ChatComposerProps) { + const [draft, setDraft] = useState(""); + const clearDraft = useCallback(() => setDraft(""), []); + + const handleSend = useCallback(async () => { + const trimmed = draft.trim(); + if (!trimmed || isSending || creationLocked) return; + await onSend(trimmed, clearDraft); + }, [creationLocked, draft, clearDraft, isSending, onSend]); + + return ( +
+ {showScrollToBottom && ( + + )} +
+ setDraft(e.target.value)} + onPressEnter={(e) => { + if (e.shiftKey) return; + e.preventDefault(); + void handleSend(); + }} + placeholder="向 ChatOne 提问" + variant="borderless" + autoSize={{ minRows: 2, maxRows: 8 }} + className="!px-1 !text-[15px] placeholder:text-neutral-400" + /> +
+
+ + +
+
+
+
+
+
+ ); +}); + export default function HomePage() { const [modal, modalContextHolder] = Modal.useModal(); const navigate = useNavigate(); @@ -127,7 +243,6 @@ export default function HomePage() { const [collapsed, setCollapsed] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); - const [inputValue, setInputValue] = useState(""); const [messages, setMessages] = useState(INITIAL_MESSAGES); const [isSending, setIsSending] = useState(false); const [deepThink, setDeepThink] = useState(() => { @@ -164,6 +279,11 @@ export default function HomePage() { const [sessionActionBusyId, setSessionActionBusyId] = useState(null); const [activeSessionId, setActiveSessionId] = useState(null); const activeSessionIdRef = useRef(null); + const messagesRef = useRef(INITIAL_MESSAGES); + + useEffect(() => { + messagesRef.current = messages; + }, [messages]); const userJsonSnapshot = useSyncExternalStore( subscribeUserProfile, @@ -287,6 +407,15 @@ export default function HomePage() { }; }, [isMessageListAtBottom]); + const toggleDeepThink = useCallback(() => setDeepThink((v) => !v), []); + const toggleSmartSearch = useCallback(() => setSmartSearch((v) => !v), []); + + const handleScrollToBottomButton = useCallback(() => { + shouldStickToBottomRef.current = true; + setShowScrollToBottom(false); + scrollMessageListToBottom("smooth"); + }, [scrollMessageListToBottom]); + useEffect(() => { if (shouldStickToBottomRef.current) { scrollMessageListToBottom("auto"); @@ -464,142 +593,153 @@ export default function HomePage() { } }, []); - const sendMessage = async () => { - const trimmed = inputValue.trim(); - if (!trimmed || isSending) return; - let targetSessionId = activeSessionId; - if (!targetSessionId) { - if (sessionMutationBusy) return; - setSessionMutationBusy(true); - try { - const row = await createChatSession({ - // 首条提问作为会话初始标题(后端限制 200)。 - title: trimmed.slice(0, 200), - }); - targetSessionId = row.id; - setActiveSessionId(row.id); - replaceSessionUrl(row.id); - // 侧栏列表后台刷新,不阻塞当前消息发送。 - void refreshSessionList(); - } catch (e) { - const text = e instanceof Error ? e.message : "新建会话失败"; - toast.error(text); - return; - } finally { - setSessionMutationBusy(false); + const sendMessage = useCallback( + async (trimmedInput: string, clearDraft: () => void) => { + const trimmed = trimmedInput.trim(); + if (!trimmed || isSending) return; + let targetSessionId = activeSessionId; + if (!targetSessionId) { + if (sessionMutationBusy) return; + setSessionMutationBusy(true); + try { + const row = await createChatSession({ + // 首条提问作为会话初始标题(后端限制 200)。 + title: trimmed.slice(0, 200), + }); + targetSessionId = row.id; + setActiveSessionId(row.id); + replaceSessionUrl(row.id); + // 侧栏列表后台刷新,不阻塞当前消息发送。 + void refreshSessionList(); + } catch (e) { + const text = e instanceof Error ? e.message : "新建会话失败"; + toast.error(text); + return; + } finally { + setSessionMutationBusy(false); + } } - } - setInputValue(""); - setIsSending(true); + clearDraft(); + setIsSending(true); - const userMessage: UiMessage = { - id: `user-${Date.now()}`, - role: "user", - content: trimmed, - }; - const assistantMessageId = `assistant-${Date.now() + 1}`; - const assistantPlaceholder: UiMessage = { - id: assistantMessageId, - role: "assistant", - content: "", - thinking: deepThink ? "正在思考并生成答案…" : undefined, - }; + const userMessage: UiMessage = { + id: `user-${Date.now()}`, + role: "user", + content: trimmed, + }; + const assistantMessageId = `assistant-${Date.now() + 1}`; + const assistantPlaceholder: UiMessage = { + id: assistantMessageId, + role: "assistant", + content: "", + thinking: deepThink ? "正在思考并生成答案…" : undefined, + }; - const nextMessages = [...messages, userMessage, assistantPlaceholder]; - setMessages(nextMessages); + const nextMessages = [...messagesRef.current, userMessage, assistantPlaceholder]; + setMessages(nextMessages); - const payloadMessages: ChatMessagePayload[] = nextMessages.map((item) => ({ - role: item.role, - content: item.content, - })); + const payloadMessages: ChatMessagePayload[] = nextMessages.map((item) => ({ + role: item.role, + content: item.content, + })); - let assistantText = ""; - let lastError: unknown = null; + let assistantText = ""; + let lastError: unknown = null; - for (let attempt = 0; attempt <= MAX_STREAM_RETRY; attempt += 1) { - const controller = new AbortController(); - abortRef.current = controller; - let receivedToken = false; + for (let attempt = 0; attempt <= MAX_STREAM_RETRY; attempt += 1) { + const controller = new AbortController(); + abortRef.current = controller; + let receivedToken = false; - const retryMessages = - attempt === 0 - ? payloadMessages - : ([ - ...nextMessages - .slice(-CONTEXT_WINDOW_SIZE) - .map((item): ChatMessagePayload => ({ role: item.role, content: item.content })), - { - role: "user" as const, - content: `网络抖动导致输出中断。请在不重复已输出内容的前提下继续回答。已输出尾部:${assistantText.slice(-80)}`, - }, - ] satisfies ChatMessagePayload[]); + const retryMessages = + attempt === 0 + ? payloadMessages + : ([ + ...nextMessages + .slice(-CONTEXT_WINDOW_SIZE) + .map((item): ChatMessagePayload => ({ role: item.role, content: item.content })), + { + role: "user" as const, + content: `网络抖动导致输出中断。请在不重复已输出内容的前提下继续回答。已输出尾部:${assistantText.slice(-80)}`, + }, + ] satisfies ChatMessagePayload[]); - try { - await streamQwenChat({ - messages: retryMessages, - model: selectedModel, - sessionId: targetSessionId ?? undefined, - enableWebSearch: smartSearch, - enableThinking: deepThink, - signal: controller.signal, - timeoutMs: STREAM_TIMEOUT_MS, - onToken: (token) => { - receivedToken = true; - assistantText += token; - setMessages((prev) => - prev.map((item) => - item.id === assistantMessageId - ? { ...item, content: `${item.content}${token}` } - : item, - ), - ); - }, - }); - lastError = null; - break; - } catch (error) { - if (error instanceof SessionExpiredError) { + try { + await streamQwenChat({ + messages: retryMessages, + model: selectedModel, + sessionId: targetSessionId ?? undefined, + enableWebSearch: smartSearch, + enableThinking: deepThink, + signal: controller.signal, + timeoutMs: STREAM_TIMEOUT_MS, + onToken: (token) => { + receivedToken = true; + assistantText += token; + setMessages((prev) => + prev.map((item) => + item.id === assistantMessageId + ? { ...item, content: `${item.content}${token}` } + : item, + ), + ); + }, + }); lastError = null; break; + } catch (error) { + if (error instanceof SessionExpiredError) { + lastError = null; + break; + } + lastError = error; + const isAbortLike = isAbortLikeError(error); + const canRetry = attempt < MAX_STREAM_RETRY; + if (!canRetry) break; + // 网络抖动或超时时自动重连;若已收到部分 token 也保留已输出文本。 + if (isAbortLike || !receivedToken) { + await new Promise((resolve) => window.setTimeout(resolve, 300)); + continue; + } + break; + } finally { + abortRef.current = null; } - lastError = error; - const isAbortLike = isAbortLikeError(error); - const canRetry = attempt < MAX_STREAM_RETRY; - if (!canRetry) break; - // 网络抖动或超时时自动重连;若已收到部分 token 也保留已输出文本。 - if (isAbortLike || !receivedToken) { - await new Promise((resolve) => window.setTimeout(resolve, 300)); - continue; - } - break; - } finally { - abortRef.current = null; } - } - if (lastError) { - const message = lastError instanceof Error ? lastError.message : "请求失败,请稍后重试"; + if (lastError) { + const message = lastError instanceof Error ? lastError.message : "请求失败,请稍后重试"; + setMessages((prev) => + prev.map((item) => + item.id === assistantMessageId + ? { + ...item, + content: item.content || `接口调用失败:${message}`, + thinking: undefined, + } + : item, + ), + ); + } + + setIsSending(false); setMessages((prev) => prev.map((item) => - item.id === assistantMessageId - ? { - ...item, - content: item.content || `接口调用失败:${message}`, - thinking: undefined, - } - : item, + item.id === assistantMessageId ? { ...item, thinking: undefined } : item, ), ); - } - - setIsSending(false); - setMessages((prev) => - prev.map((item) => - item.id === assistantMessageId ? { ...item, thinking: undefined } : item, - ), - ); - }; + }, + [ + activeSessionId, + deepThink, + isSending, + refreshSessionList, + selectedModel, + sessionMutationBusy, + smartSearch, + ], + ); const sidebarContent = (
@@ -618,7 +758,7 @@ export default function HomePage() {
-
+
{sessionsLoading ? ( 加载会话… @@ -808,12 +948,9 @@ export default function HomePage() { aria-label="选择对话模型" />
- - 快速模式 - - +
-
- {showScrollToBottom && ( - - )} -
- setInputValue(e.target.value)} - onPressEnter={(e) => { - if (e.shiftKey) return; - e.preventDefault(); - void sendMessage(); - }} - placeholder="给 ChatOne 发送消息" - variant="borderless" - autoSize={{ minRows: 2, maxRows: 8 }} - className="!px-1 !text-[15px] placeholder:text-neutral-400" - /> -
-
- - -
-
-
-
-
-
+

内容由 AI 生成,请注意核实

@@ -1049,7 +1117,7 @@ export default function HomePage() { closable onClose={() => setMobileSidebarOpen(false)} open={isMobile && mobileSidebarOpen} - width={280} + size={280} styles={{ body: { padding: 0,