perf(home): 拆分 ChatComposer 修复输入框打字卡顿
All checks were successful
CI / build (push) Successful in 3m7s
All checks were successful
CI / build (push) Successful in 3m7s
将输入草稿状态从 HomePage 抽离为独立 memo 组件,避免每次按键重渲染消息列表与侧栏;并微调输入区与滚动条样式。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,15 @@
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
||||||
import {
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useSyncExternalStore,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Atom,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -13,7 +22,7 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Pin,
|
Pin,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Search,
|
Globe,
|
||||||
Settings,
|
Settings,
|
||||||
Share2,
|
Share2,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
@@ -120,6 +129,113 @@ function replaceSessionUrl(sessionId: string) {
|
|||||||
window.history.replaceState(window.history.state, "", nextPath);
|
window.history.replaceState(window.history.state, "", nextPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatComposerProps = {
|
||||||
|
onSend: (text: string, clearDraft: () => void) => Promise<void>;
|
||||||
|
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 (
|
||||||
|
<div className="relative">
|
||||||
|
{showScrollToBottom && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="滚动到底部"
|
||||||
|
className="absolute -top-14 right-3 z-10 flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-neutral-200/90 bg-white text-neutral-900 shadow-md transition-colors hover:bg-neutral-50"
|
||||||
|
onClick={onScrollToBottomClick}
|
||||||
|
>
|
||||||
|
<ChevronDown color="#666" size={18} strokeWidth={2} aria-hidden />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="rounded-4xl border border-[var(--ds-border)] p-3 shadow-xs">
|
||||||
|
<Input.TextArea
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
color={deepThink ? "primary" : "default"}
|
||||||
|
variant={deepThink ? "filled" : undefined}
|
||||||
|
shape="round"
|
||||||
|
icon={<Atom size={14} />}
|
||||||
|
onClick={onDeepThinkToggle}
|
||||||
|
>
|
||||||
|
深度思考
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={smartSearch ? "primary" : "default"}
|
||||||
|
variant={smartSearch ? "filled" : undefined}
|
||||||
|
shape="round"
|
||||||
|
icon={<Globe size={14} />}
|
||||||
|
onClick={onSmartSearchToggle}
|
||||||
|
>
|
||||||
|
智能搜索
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
icon={<Paperclip size={16} className="text-neutral-400" />}
|
||||||
|
className="text-neutral-400"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon={<ArrowUp size={18} />}
|
||||||
|
loading={isSending}
|
||||||
|
disabled={isSending || creationLocked || !draft.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
void handleSend();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -127,7 +243,6 @@ export default function HomePage() {
|
|||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState("");
|
|
||||||
const [messages, setMessages] = useState<UiMessage[]>(INITIAL_MESSAGES);
|
const [messages, setMessages] = useState<UiMessage[]>(INITIAL_MESSAGES);
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const [deepThink, setDeepThink] = useState(() => {
|
const [deepThink, setDeepThink] = useState(() => {
|
||||||
@@ -164,6 +279,11 @@ export default function HomePage() {
|
|||||||
const [sessionActionBusyId, setSessionActionBusyId] = useState<string | null>(null);
|
const [sessionActionBusyId, setSessionActionBusyId] = useState<string | null>(null);
|
||||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||||
const activeSessionIdRef = useRef<string | null>(null);
|
const activeSessionIdRef = useRef<string | null>(null);
|
||||||
|
const messagesRef = useRef<UiMessage[]>(INITIAL_MESSAGES);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesRef.current = messages;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const userJsonSnapshot = useSyncExternalStore(
|
const userJsonSnapshot = useSyncExternalStore(
|
||||||
subscribeUserProfile,
|
subscribeUserProfile,
|
||||||
@@ -287,6 +407,15 @@ export default function HomePage() {
|
|||||||
};
|
};
|
||||||
}, [isMessageListAtBottom]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (shouldStickToBottomRef.current) {
|
if (shouldStickToBottomRef.current) {
|
||||||
scrollMessageListToBottom("auto");
|
scrollMessageListToBottom("auto");
|
||||||
@@ -464,142 +593,153 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = useCallback(
|
||||||
const trimmed = inputValue.trim();
|
async (trimmedInput: string, clearDraft: () => void) => {
|
||||||
if (!trimmed || isSending) return;
|
const trimmed = trimmedInput.trim();
|
||||||
let targetSessionId = activeSessionId;
|
if (!trimmed || isSending) return;
|
||||||
if (!targetSessionId) {
|
let targetSessionId = activeSessionId;
|
||||||
if (sessionMutationBusy) return;
|
if (!targetSessionId) {
|
||||||
setSessionMutationBusy(true);
|
if (sessionMutationBusy) return;
|
||||||
try {
|
setSessionMutationBusy(true);
|
||||||
const row = await createChatSession({
|
try {
|
||||||
// 首条提问作为会话初始标题(后端限制 200)。
|
const row = await createChatSession({
|
||||||
title: trimmed.slice(0, 200),
|
// 首条提问作为会话初始标题(后端限制 200)。
|
||||||
});
|
title: trimmed.slice(0, 200),
|
||||||
targetSessionId = row.id;
|
});
|
||||||
setActiveSessionId(row.id);
|
targetSessionId = row.id;
|
||||||
replaceSessionUrl(row.id);
|
setActiveSessionId(row.id);
|
||||||
// 侧栏列表后台刷新,不阻塞当前消息发送。
|
replaceSessionUrl(row.id);
|
||||||
void refreshSessionList();
|
// 侧栏列表后台刷新,不阻塞当前消息发送。
|
||||||
} catch (e) {
|
void refreshSessionList();
|
||||||
const text = e instanceof Error ? e.message : "新建会话失败";
|
} catch (e) {
|
||||||
toast.error(text);
|
const text = e instanceof Error ? e.message : "新建会话失败";
|
||||||
return;
|
toast.error(text);
|
||||||
} finally {
|
return;
|
||||||
setSessionMutationBusy(false);
|
} finally {
|
||||||
|
setSessionMutationBusy(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setInputValue("");
|
clearDraft();
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
const userMessage: UiMessage = {
|
const userMessage: UiMessage = {
|
||||||
id: `user-${Date.now()}`,
|
id: `user-${Date.now()}`,
|
||||||
role: "user",
|
role: "user",
|
||||||
content: trimmed,
|
content: trimmed,
|
||||||
};
|
};
|
||||||
const assistantMessageId = `assistant-${Date.now() + 1}`;
|
const assistantMessageId = `assistant-${Date.now() + 1}`;
|
||||||
const assistantPlaceholder: UiMessage = {
|
const assistantPlaceholder: UiMessage = {
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
thinking: deepThink ? "正在思考并生成答案…" : undefined,
|
thinking: deepThink ? "正在思考并生成答案…" : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextMessages = [...messages, userMessage, assistantPlaceholder];
|
const nextMessages = [...messagesRef.current, userMessage, assistantPlaceholder];
|
||||||
setMessages(nextMessages);
|
setMessages(nextMessages);
|
||||||
|
|
||||||
const payloadMessages: ChatMessagePayload[] = nextMessages.map((item) => ({
|
const payloadMessages: ChatMessagePayload[] = nextMessages.map((item) => ({
|
||||||
role: item.role,
|
role: item.role,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let assistantText = "";
|
let assistantText = "";
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= MAX_STREAM_RETRY; attempt += 1) {
|
for (let attempt = 0; attempt <= MAX_STREAM_RETRY; attempt += 1) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
abortRef.current = controller;
|
abortRef.current = controller;
|
||||||
let receivedToken = false;
|
let receivedToken = false;
|
||||||
|
|
||||||
const retryMessages =
|
const retryMessages =
|
||||||
attempt === 0
|
attempt === 0
|
||||||
? payloadMessages
|
? payloadMessages
|
||||||
: ([
|
: ([
|
||||||
...nextMessages
|
...nextMessages
|
||||||
.slice(-CONTEXT_WINDOW_SIZE)
|
.slice(-CONTEXT_WINDOW_SIZE)
|
||||||
.map((item): ChatMessagePayload => ({ role: item.role, content: item.content })),
|
.map((item): ChatMessagePayload => ({ role: item.role, content: item.content })),
|
||||||
{
|
{
|
||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
content: `网络抖动导致输出中断。请在不重复已输出内容的前提下继续回答。已输出尾部:${assistantText.slice(-80)}`,
|
content: `网络抖动导致输出中断。请在不重复已输出内容的前提下继续回答。已输出尾部:${assistantText.slice(-80)}`,
|
||||||
},
|
},
|
||||||
] satisfies ChatMessagePayload[]);
|
] satisfies ChatMessagePayload[]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await streamQwenChat({
|
await streamQwenChat({
|
||||||
messages: retryMessages,
|
messages: retryMessages,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
sessionId: targetSessionId ?? undefined,
|
sessionId: targetSessionId ?? undefined,
|
||||||
enableWebSearch: smartSearch,
|
enableWebSearch: smartSearch,
|
||||||
enableThinking: deepThink,
|
enableThinking: deepThink,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
timeoutMs: STREAM_TIMEOUT_MS,
|
timeoutMs: STREAM_TIMEOUT_MS,
|
||||||
onToken: (token) => {
|
onToken: (token) => {
|
||||||
receivedToken = true;
|
receivedToken = true;
|
||||||
assistantText += token;
|
assistantText += token;
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((item) =>
|
prev.map((item) =>
|
||||||
item.id === assistantMessageId
|
item.id === assistantMessageId
|
||||||
? { ...item, content: `${item.content}${token}` }
|
? { ...item, content: `${item.content}${token}` }
|
||||||
: item,
|
: item,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
lastError = null;
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof SessionExpiredError) {
|
|
||||||
lastError = null;
|
lastError = null;
|
||||||
break;
|
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) {
|
if (lastError) {
|
||||||
const message = lastError instanceof Error ? lastError.message : "请求失败,请稍后重试";
|
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) =>
|
setMessages((prev) =>
|
||||||
prev.map((item) =>
|
prev.map((item) =>
|
||||||
item.id === assistantMessageId
|
item.id === assistantMessageId ? { ...item, thinking: undefined } : item,
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
content: item.content || `接口调用失败:${message}`,
|
|
||||||
thinking: undefined,
|
|
||||||
}
|
|
||||||
: item,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
[
|
||||||
setIsSending(false);
|
activeSessionId,
|
||||||
setMessages((prev) =>
|
deepThink,
|
||||||
prev.map((item) =>
|
isSending,
|
||||||
item.id === assistantMessageId ? { ...item, thinking: undefined } : item,
|
refreshSessionList,
|
||||||
),
|
selectedModel,
|
||||||
);
|
sessionMutationBusy,
|
||||||
};
|
smartSearch,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col bg-[var(--ds-bg-sider)]">
|
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col bg-[var(--ds-bg-sider)]">
|
||||||
@@ -618,7 +758,7 @@ export default function HomePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto px-2 py-2">
|
<div className="min-h-0 flex-1 overflow-auto px-2 py-2 scrollbar-thumb-gray-200 scrollbar-gutter-stable">
|
||||||
{sessionsLoading ? (
|
{sessionsLoading ? (
|
||||||
<Typography.Text className="block px-2 text-[12px]" type="secondary">
|
<Typography.Text className="block px-2 text-[12px]" type="secondary">
|
||||||
加载会话…
|
加载会话…
|
||||||
@@ -808,12 +948,9 @@ export default function HomePage() {
|
|||||||
aria-label="选择对话模型"
|
aria-label="选择对话模型"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full border border-[var(--ds-border)] bg-neutral-50 px-3 py-1 text-xs text-neutral-500">
|
|
||||||
快速模式
|
|
||||||
</span>
|
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Content className="flex min-h-0 flex-1 flex-col overflow-hidden !bg-[var(--ds-bg-main)]">
|
<Content className="flex min-h-0 flex-1 flex-col overflow-hidden !bg-[var(--ds-bg-main)] scrollbar-thumb-gray-200 scrollbar-gutter-stable">
|
||||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
ref={messageListRef}
|
ref={messageListRef}
|
||||||
@@ -954,86 +1091,17 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<div className="shrink-0 bg-[var(--ds-bg-main)] px-4 pt-0 md:px-8 md:pt-0">
|
<div className="shrink-0 bg-[var(--ds-bg-main)] px-4 pt-0 md:px-8 md:pt-0">
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<div className="relative">
|
<ChatComposer
|
||||||
{showScrollToBottom && (
|
onSend={sendMessage}
|
||||||
<button
|
isSending={isSending}
|
||||||
type="button"
|
creationLocked={!activeSessionId && sessionMutationBusy}
|
||||||
aria-label="滚动到底部"
|
deepThink={deepThink}
|
||||||
className="absolute -top-14 right-3 z-10 flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-neutral-200/90 bg-white text-neutral-900 shadow-md transition-colors hover:bg-neutral-50"
|
onDeepThinkToggle={toggleDeepThink}
|
||||||
onClick={() => {
|
smartSearch={smartSearch}
|
||||||
shouldStickToBottomRef.current = true;
|
onSmartSearchToggle={toggleSmartSearch}
|
||||||
setShowScrollToBottom(false);
|
showScrollToBottom={showScrollToBottom}
|
||||||
scrollMessageListToBottom("smooth");
|
onScrollToBottomClick={handleScrollToBottomButton}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
<ChevronDown color="#666" size={18} strokeWidth={2} aria-hidden />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="rounded-3xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm">
|
|
||||||
<Input.TextArea
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setDeepThink((v) => !v)}
|
|
||||||
className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
|
|
||||||
deepThink
|
|
||||||
? "border-sky-300 bg-sky-50 text-sky-700"
|
|
||||||
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Zap size={14} />
|
|
||||||
深度思考
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSmartSearch((v) => !v)}
|
|
||||||
className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
|
|
||||||
smartSearch
|
|
||||||
? "border-sky-300 bg-sky-50 text-sky-700"
|
|
||||||
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Search size={14} />
|
|
||||||
智能搜索
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<Paperclip size={16} className="text-neutral-400" />}
|
|
||||||
className="text-neutral-400"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
shape="circle"
|
|
||||||
icon={<ArrowUp size={16} />}
|
|
||||||
loading={isSending}
|
|
||||||
disabled={isSending || !inputValue.trim()}
|
|
||||||
onClick={() => {
|
|
||||||
void sendMessage();
|
|
||||||
}}
|
|
||||||
className="!flex !h-9 !w-9 !items-center !justify-center !border-0 !shadow-none"
|
|
||||||
style={{
|
|
||||||
background: "var(--ds-send)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="m-0 py-2 text-center text-[11px] text-neutral-500">
|
<p className="m-0 py-2 text-center text-[11px] text-neutral-500">
|
||||||
内容由 AI 生成,请注意核实
|
内容由 AI 生成,请注意核实
|
||||||
</p>
|
</p>
|
||||||
@@ -1049,7 +1117,7 @@ export default function HomePage() {
|
|||||||
closable
|
closable
|
||||||
onClose={() => setMobileSidebarOpen(false)}
|
onClose={() => setMobileSidebarOpen(false)}
|
||||||
open={isMobile && mobileSidebarOpen}
|
open={isMobile && mobileSidebarOpen}
|
||||||
width={280}
|
size={280}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user