feat(chat): 消息列表回到底部与贴底滚动优化
All checks were successful
CI / build (push) Successful in 2m7s

- 非底部时显示悬浮回到底部按钮,点击平滑滚底并隐藏
- 仅在已处于底部附近时随新消息自动贴底,避免打断上翻阅读
- 按钮使用原生圆钮并相对输入区 max-w-4xl 定位,避免 AntD 按钮宽条问题
- 同步 lucide-react 依赖及流式代码块、全局样式等小改动

Made-with: Cursor
This commit is contained in:
2026-04-24 04:39:24 +08:00
parent 3b0b6eac50
commit 891f09aa0d
5 changed files with 312 additions and 246 deletions

View File

@@ -30,6 +30,7 @@
"axios": "^1.15.0", "axios": "^1.15.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"katex": "^0.16.45", "katex": "^0.16.45",
"lucide-react": "^1.9.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
import { Button, message } from "antd"; import { Button, message } from "antd";
import { Check, Copy } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
@@ -47,7 +47,7 @@ function MarkdownPreBlock(props: { children: ReactNode }) {
<Button <Button
type="text" type="text"
size="small" size="small"
icon={copied ? <CheckOutlined /> : <CopyOutlined />} icon={copied ? <Check size={16} /> : <Copy size={16} />}
className="text-neutral-600 hover:bg-black/5! hover:text-neutral-800!" className="text-neutral-600 hover:bg-black/5! hover:text-neutral-800!"
onClick={() => { onClick={() => {
void onCopy(); void onCopy();

View File

@@ -83,3 +83,9 @@ pre code.hljs {
.ant-dropdown-menu-item-danger { .ant-dropdown-menu-item-danger {
border-radius: 12px !important; border-radius: 12px !important;
} }
/* 修正 lucide 图标在 AntD Button 中的基线偏移 */
.ant-btn .ant-btn-icon > .lucide {
display: block;
transform: translateY(1px);
}

View File

@@ -1,26 +1,27 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { import {
ArrowUpOutlined, ArrowUp,
CopyOutlined, ChevronDown,
DeleteOutlined, Copy,
EditOutlined, Ellipsis,
LogoutOutlined, LogOut,
MenuFoldOutlined, MessageCirclePlus,
MenuUnfoldOutlined, Paperclip,
MobileOutlined, PanelLeftClose,
MoreOutlined, PanelLeftOpen,
PaperClipOutlined, Pencil,
PlusOutlined, Pin,
QuestionCircleOutlined, RotateCcw,
RedoOutlined, Search,
SearchOutlined, Settings,
ShareAltOutlined, Share2,
SettingOutlined, Smartphone,
ThunderboltOutlined, Trash2,
VerticalAlignTopOutlined, User,
UserOutlined, Zap,
} from "@ant-design/icons"; CircleHelp,
} from "lucide-react";
import { import {
Avatar, Avatar,
Button, Button,
@@ -88,13 +89,7 @@ function subscribeUserProfile(onChange: () => void) {
}; };
} }
const INITIAL_MESSAGES: UiMessage[] = [ const INITIAL_MESSAGES: UiMessage[] = [];
{
id: "init-assistant",
role: "assistant",
content: "你好,我是 ChatOne 助手,有什么可以帮你?",
},
];
const STREAM_TIMEOUT_MS = 90000; const STREAM_TIMEOUT_MS = 90000;
const MAX_STREAM_RETRY = 1; const MAX_STREAM_RETRY = 1;
@@ -155,7 +150,9 @@ export default function HomePage() {
} }
}); });
const messageListRef = useRef<HTMLDivElement | null>(null); const messageListRef = useRef<HTMLDivElement | null>(null);
const shouldStickToBottomRef = useRef(true);
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [sessions, setSessions] = useState<ChatSession[]>([]); const [sessions, setSessions] = useState<ChatSession[]>([]);
const [sessionsLoading, setSessionsLoading] = useState(false); const [sessionsLoading, setSessionsLoading] = useState(false);
@@ -200,18 +197,18 @@ export default function HomePage() {
const userMenuDividerStyle: CSSProperties = { margin: "10px 0" }; const userMenuDividerStyle: CSSProperties = { margin: "10px 0" };
const userMenuItems: MenuProps["items"] = [ const userMenuItems: MenuProps["items"] = [
{ key: "app", icon: <MobileOutlined />, label: "下载手机应用", style: userMenuItemStyle }, { key: "app", icon: <Smartphone size={16} />, label: "下载手机应用", style: userMenuItemStyle },
{ key: "settings", icon: <SettingOutlined />, label: "系统设置", style: userMenuItemStyle }, { key: "settings", icon: <Settings size={16} />, label: "系统设置", style: userMenuItemStyle },
{ {
key: "help", key: "help",
icon: <QuestionCircleOutlined />, icon: <CircleHelp size={16} />,
label: "帮助与反馈", label: "帮助与反馈",
style: userMenuItemStyle, style: userMenuItemStyle,
}, },
{ type: "divider", style: userMenuDividerStyle }, { type: "divider", style: userMenuDividerStyle },
{ {
key: "logout", key: "logout",
icon: <LogoutOutlined />, icon: <LogOut size={16} />,
label: "退出登录", label: "退出登录",
danger: true, danger: true,
style: userMenuItemStyle, style: userMenuItemStyle,
@@ -258,11 +255,42 @@ export default function HomePage() {
}; };
}, []); }, []);
const isMessageListAtBottom = useCallback(() => {
const el = messageListRef.current;
if (!el) return true;
// 允许少量像素误差,避免小数像素造成闪烁。
return el.scrollHeight - el.scrollTop - el.clientHeight <= 24;
}, []);
const scrollMessageListToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
const el = messageListRef.current;
if (!el) return;
el.scrollTo({ top: el.scrollHeight, behavior });
}, []);
useEffect(() => { useEffect(() => {
const el = messageListRef.current; const el = messageListRef.current;
if (!el) return; if (!el) return;
el.scrollTop = el.scrollHeight; const onScroll = () => {
}, [messages]); const atBottom = isMessageListAtBottom();
shouldStickToBottomRef.current = atBottom;
setShowScrollToBottom(!atBottom);
};
onScroll();
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
el.removeEventListener("scroll", onScroll);
};
}, [isMessageListAtBottom]);
useEffect(() => {
if (shouldStickToBottomRef.current) {
scrollMessageListToBottom("auto");
setShowScrollToBottom(false);
} else {
setShowScrollToBottom(true);
}
}, [messages, scrollMessageListToBottom]);
const refreshSessionList = useCallback(async (): Promise<ChatSession[]> => { const refreshSessionList = useCallback(async (): Promise<ChatSession[]> => {
if (!window.localStorage.getItem(ACCESS_TOKEN_KEY)) return []; if (!window.localStorage.getItem(ACCESS_TOKEN_KEY)) return [];
@@ -567,7 +595,7 @@ export default function HomePage() {
loading={sessionMutationBusy} loading={sessionMutationBusy}
disabled={sessionsLoading} disabled={sessionsLoading}
className="h-10! rounded-xl! border-[var(--ds-border)]! bg-[var(--ds-bg-main)]! text-[13px]! font-medium shadow-none hover:border-sky-300! hover:text-sky-600!" className="h-10! rounded-xl! border-[var(--ds-border)]! bg-[var(--ds-bg-main)]! text-[13px]! font-medium shadow-none hover:border-sky-300! hover:text-sky-600!"
icon={<PlusOutlined />} icon={<MessageCirclePlus size={14} strokeWidth={2} />}
onClick={() => { onClick={() => {
void handleNewSession(); void handleNewSession();
}} }}
@@ -600,9 +628,9 @@ export default function HomePage() {
const active = sid === activeSessionId; const active = sid === activeSessionId;
const itemMenu: MenuProps = { const itemMenu: MenuProps = {
items: [ items: [
{ key: "rename", icon: <EditOutlined />, label: "重命名" }, { key: "rename", icon: <Pencil size={16} />, label: "重命名" },
{ key: "pin", icon: <VerticalAlignTopOutlined />, label: "置顶" }, { key: "pin", icon: <Pin size={16} />, label: "置顶" },
{ key: "delete", icon: <DeleteOutlined />, danger: true, label: "删除" }, { key: "delete", icon: <Trash2 size={16} />, danger: true, label: "删除" },
], ],
onClick: ({ key, domEvent }) => { onClick: ({ key, domEvent }) => {
domEvent.stopPropagation(); domEvent.stopPropagation();
@@ -614,7 +642,7 @@ export default function HomePage() {
return ( return (
<div <div
key={sid} key={sid}
className="mb-1 flex w-full items-center gap-1 rounded-xl pr-1 text-[14px] text-neutral-700 transition-colors hover:bg-black/5" className="group mb-1.5 flex w-full items-center gap-1 rounded-2xl pr-1 text-[14px] text-neutral-700 transition-colors hover:bg-black/5"
style={{ style={{
background: active ? "var(--ds-active-item)" : undefined, background: active ? "var(--ds-active-item)" : undefined,
color: active ? "var(--ds-accent)" : undefined, color: active ? "var(--ds-accent)" : undefined,
@@ -625,23 +653,29 @@ export default function HomePage() {
onClick={() => { onClick={() => {
void openSession(sid); void openSession(sid);
}} }}
className="flex min-h-12 min-w-0 flex-1 items-center rounded-xl px-3 py-3 text-left" className="flex h-11 min-w-0 flex-1 cursor-pointer items-center rounded-2xl px-4 text-left"
> >
{(!collapsed || isMobile) && <span className="truncate">{title}</span>} {(!collapsed || isMobile) && (
<span className="min-w-0 flex-1 truncate text-[14px] leading-none">
{title}
</span>
)}
</button> </button>
{(!collapsed || isMobile) && ( {(!collapsed || isMobile) && (
<Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight"> <div className={`${active ? "flex" : "hidden group-hover:flex"}`}>
<Button <Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight">
type="text" <Button
size="small" type="text"
icon={<MoreOutlined />} size="small"
loading={sessionActionBusyId === sid} icon={<Ellipsis size={16} />}
className="text-neutral-500 hover:bg-black/5!" loading={sessionActionBusyId === sid}
onClick={(event) => { className="!flex !h-8 !w-8 !items-center !justify-center rounded-xl text-neutral-500 hover:bg-black/5!"
event.stopPropagation(); onClick={(event) => {
}} event.stopPropagation();
/> }}
</Dropdown> />
</Dropdown>
</div>
)} )}
</div> </div>
); );
@@ -650,7 +684,7 @@ export default function HomePage() {
)} )}
</div> </div>
<div className="shrink-0 border-t border-[var(--ds-border)] p-3"> <div className="shrink-0 p-3">
<Dropdown <Dropdown
menu={{ items: userMenuItems, onClick: onUserMenuClick }} menu={{ items: userMenuItems, onClick: onUserMenuClick }}
placement="topLeft" placement="topLeft"
@@ -658,14 +692,14 @@ export default function HomePage() {
> >
<button <button
type="button" type="button"
className={`flex w-full items-center gap-2 rounded-xl bg-neutral-100 px-2 py-2 text-left transition-colors hover:bg-neutral-200/90 ${ className={`flex w-full cursor-pointer items-center gap-2 rounded-xl bg-neutral-100 px-2 py-2 text-left transition-colors hover:bg-neutral-200/90 ${
collapsed && !isMobile ? "justify-center py-2.5" : "" collapsed && !isMobile ? "justify-center py-2.5" : ""
}`} }`}
> >
<Avatar <Avatar
size={36} size={36}
src={userProfile.avatar} src={userProfile.avatar}
icon={!userProfile.avatar ? <UserOutlined /> : undefined} icon={!userProfile.avatar ? <User size={18} /> : undefined}
className="shrink-0 bg-neutral-300" className="shrink-0 bg-neutral-300"
/> />
{(!collapsed || isMobile) && ( {(!collapsed || isMobile) && (
@@ -673,7 +707,7 @@ export default function HomePage() {
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800"> <span className="min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800">
{userProfile.displayName} {userProfile.displayName}
</span> </span>
<MoreOutlined className="shrink-0 text-neutral-500" /> <Ellipsis size={16} className="shrink-0 text-neutral-500" />
</> </>
)} )}
</button> </button>
@@ -696,10 +730,10 @@ export default function HomePage() {
width={260} width={260}
collapsedWidth={72} collapsedWidth={72}
theme="light" theme="light"
className="!min-h-0 !h-full !overflow-hidden !bg-[var(--ds-bg-sider)] !border-r !border-[var(--ds-border)]" className="!min-h-0 !h-full !overflow-hidden !bg-[var(--ds-bg-sider)]"
> >
<div className="flex h-full min-h-0 min-w-0 flex-col"> <div className="flex h-full min-h-0 min-w-0 flex-col">
<div className="flex h-14 shrink-0 items-center gap-2 border-b border-[var(--ds-border)] px-3"> <div className="flex h-14 shrink-0 items-center gap-2 px-3">
<img <img
src="/logo.png" src="/logo.png"
alt="" alt=""
@@ -724,7 +758,7 @@ export default function HomePage() {
style={{ background: "var(--ds-bg-main)" }} style={{ background: "var(--ds-bg-main)" }}
> >
<Header <Header
className="!flex !h-14 !shrink-0 !items-center !justify-between !border-b !border-[var(--ds-border)] !bg-[var(--ds-bg-main)] !px-5" className="!flex !h-14 !shrink-0 !items-center !justify-between !bg-[var(--ds-bg-main)] !px-5"
style={{ lineHeight: "56px" }} style={{ lineHeight: "56px" }}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -733,11 +767,11 @@ export default function HomePage() {
className="text-neutral-500 hover:bg-black/5!" className="text-neutral-500 hover:bg-black/5!"
icon={ icon={
isMobile ? ( isMobile ? (
<MenuUnfoldOutlined /> <PanelLeftOpen size={16} />
) : collapsed ? ( ) : collapsed ? (
<MenuUnfoldOutlined /> <PanelLeftOpen size={16} />
) : ( ) : (
<MenuFoldOutlined /> <PanelLeftClose size={16} />
) )
} }
onClick={() => { onClick={() => {
@@ -770,203 +804,223 @@ export default function HomePage() {
</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)]">
<div className="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}
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 pt-6 pb-0 md:px-8 md:pt-6 md:pb-0" className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 pt-6 pb-0 md:px-8 md:pt-6 md:pb-0"
> >
<div className="mx-auto flex max-w-4xl flex-col gap-5"> <div className="mx-auto flex max-w-4xl flex-col gap-5">
{messages.map((item) => ( {!activeSessionId && messages.length === 0 ? (
<div <p className="px-4 text-[18px] text-neutral-800"> ChatOne</p>
key={item.id} ) : (
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`} messages.map((item) => (
>
<div <div
className={ key={item.id}
item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2" className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
}
> >
{item.role === "user" && ( <div
<div className={
className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800" item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2"
style={{ background: "var(--ds-user-bubble)" }} }
> >
{item.content} {item.role === "user" && (
</div> <div
)} className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
{item.role === "assistant" && item.thinking && ( style={{ background: "var(--ds-user-bubble)" }}
<Collapse >
bordered={false} {item.content}
size="small"
className="!mb-2 !rounded-xl !bg-transparent"
items={[
{
key: "think",
label: (
<span className="text-[13px] text-neutral-600">
<ThunderboltOutlined className="mr-1.5" />
2
</span>
),
children: (
<Typography.Text type="secondary" className="text-[13px]">
{item.thinking}
</Typography.Text>
),
},
]}
/>
)}
{item.role === "assistant" &&
(item.content ? (
<StreamMessage content={item.content} />
) : (
<div className="rounded-2xl px-4 py-3 text-[14px] text-neutral-500">
<span className="inline-flex items-center gap-1">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
</span>
</div> </div>
))} )}
{item.role === "assistant" && item.content && ( {item.role === "assistant" && item.thinking && (
<> <Collapse
<p className="mt-3 px-4 text-[12px] text-neutral-500 italic"> bordered={false}
AI size="small"
</p> className="!mb-2 !rounded-xl !bg-transparent"
<div className="mt-2 flex h-8 items-center gap-1 px-4 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto"> items={[
<Tooltip title="复制"> {
<Button key: "think",
type="text" label: (
size="small" <span className="text-[13px] text-neutral-600">
icon={<CopyOutlined />} <Zap size={14} className="mr-1.5" />
className="!px-1 text-neutral-500 hover:bg-black/5!" 2
aria-label="复制回复" </span>
onClick={() => { ),
void handleCopyAssistantMessage(item.content); children: (
}} <Typography.Text type="secondary" className="text-[13px]">
/> {item.thinking}
</Tooltip> </Typography.Text>
<Tooltip title="重新生成"> ),
<Button },
type="text" ]}
size="small" />
icon={<RedoOutlined />} )}
className="!px-1 text-neutral-500 hover:bg-black/5!" {item.role === "assistant" &&
aria-label="重新生成" (item.content ? (
onClick={() => { <StreamMessage content={item.content} />
message.info("重新生成功能开发中"); ) : (
}} <div className="rounded-2xl px-4 py-3 text-[14px] text-neutral-500">
/> <span className="inline-flex items-center gap-1">
</Tooltip> <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<Tooltip title="分享"> <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<Button <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
type="text" </span>
size="small" </div>
icon={<ShareAltOutlined />} ))}
className="!px-1 text-neutral-500 hover:bg-black/5!" {item.role === "assistant" && item.content && (
aria-label="分享" <>
onClick={() => { <p className="mt-3 px-4 text-[12px] text-neutral-500 italic">
message.info("分享功能开发中"); AI
}} </p>
/> <div className="mt-2 flex h-8 items-center gap-1 px-4 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
</Tooltip> <Tooltip title="复制">
<Button
type="text"
size="small"
icon={<Copy size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="复制回复"
onClick={() => {
void handleCopyAssistantMessage(item.content);
}}
/>
</Tooltip>
<Tooltip title="重新生成">
<Button
type="text"
size="small"
icon={<RotateCcw size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="重新生成"
onClick={() => {
message.info("重新生成功能开发中");
}}
/>
</Tooltip>
<Tooltip title="分享">
<Button
type="text"
size="small"
icon={<Share2 size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="分享"
onClick={() => {
message.info("分享功能开发中");
}}
/>
</Tooltip>
</div>
</>
)}
{item.role === "user" && (
<div className="mt-2 flex h-7 items-center justify-end gap-1 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Button
type="text"
size="small"
icon={<Copy size={16} />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="复制消息"
onClick={() => {
void handleCopyUserMessage(item.content);
}}
/>
<Button
type="text"
size="small"
icon={<Pencil size={16} />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="编辑消息"
onClick={() => {
message.info("编辑功能开发中");
}}
/>
</div> </div>
</> )}
)} </div>
{item.role === "user" && (
<div className="mt-2 flex h-7 items-center justify-end gap-1 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="复制消息"
onClick={() => {
void handleCopyUserMessage(item.content);
}}
/>
<Button
type="text"
size="small"
icon={<EditOutlined />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="编辑消息"
onClick={() => {
message.info("编辑功能开发中");
}}
/>
</div>
)}
</div> </div>
</div> ))
))} )}
</div> </div>
</div> </div>
<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="rounded-3xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm"> <div className="relative">
<Input.TextArea {showScrollToBottom && (
value={inputValue} <button
onChange={(e) => setInputValue(e.target.value)} type="button"
onPressEnter={(e) => { aria-label="滚动到底部"
if (e.shiftKey) return; 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"
e.preventDefault(); onClick={() => {
void sendMessage(); shouldStickToBottomRef.current = true;
}} setShowScrollToBottom(false);
placeholder="给 ChatOne 发送消息" scrollMessageListToBottom("smooth");
variant="borderless" }}
autoSize={{ minRows: 2, maxRows: 8 }} >
className="!px-1 !text-[15px] placeholder:text-neutral-400" <ChevronDown color="#666" size={18} strokeWidth={2} aria-hidden />
/> </button>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2"> )}
<div className="flex flex-wrap gap-2"> <div className="rounded-3xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm">
<button <Input.TextArea
type="button" value={inputValue}
onClick={() => setDeepThink((v) => !v)} onChange={(e) => setInputValue(e.target.value)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${ onPressEnter={(e) => {
deepThink if (e.shiftKey) return;
? "border-sky-300 bg-sky-50 text-sky-700" e.preventDefault();
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300" void sendMessage();
}`} }}
> placeholder="给 ChatOne 发送消息"
<ThunderboltOutlined /> variant="borderless"
autoSize={{ minRows: 2, maxRows: 8 }}
</button> className="!px-1 !text-[15px] placeholder:text-neutral-400"
<button />
type="button" <div className="mt-2 flex flex-wrap items-center justify-between gap-2">
onClick={() => setSmartSearch((v) => !v)} <div className="flex flex-wrap gap-2">
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${ <button
smartSearch type="button"
? "border-sky-300 bg-sky-50 text-sky-700" onClick={() => setDeepThink((v) => !v)}
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300" 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"
<SearchOutlined /> : "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
}`}
</button> >
</div> <Zap size={14} />
<div className="flex items-center gap-2">
<Button </button>
type="text" <button
icon={<PaperClipOutlined className="text-neutral-400" />} type="button"
className="text-neutral-400" 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 ${
<Button smartSearch
type="primary" ? "border-sky-300 bg-sky-50 text-sky-700"
shape="circle" : "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
icon={<ArrowUpOutlined />} }`}
loading={isSending} >
disabled={isSending || !inputValue.trim()} <Search size={14} />
onClick={() => {
void sendMessage(); </button>
}} </div>
className="!flex !h-9 !w-9 !items-center !justify-center !border-0 !shadow-none" <div className="flex items-center gap-2">
style={{ <Button
background: "var(--ds-send)", 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> </div>
</div> </div>

View File

@@ -2932,6 +2932,11 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
lucide-react@^1.9.0:
version "1.9.0"
resolved "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.9.0.tgz#3c8324e6b574131624c1869cbd38829d9c659627"
integrity sha512-6qVAmbgCjcJz7sAGSPSSJ++RAwjlK2XCbRrZKv63Ciko1KT8jX0//CXxgI3jg2HlJu8tADqdYlNDebmYjeoruA==
magic-string@^0.30.21: magic-string@^0.30.21:
version "0.30.21" version "0.30.21"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"