- 非底部时显示悬浮回到底部按钮,点击平滑滚底并隐藏 - 仅在已处于底部附近时随新消息自动贴底,避免打断上翻阅读 - 按钮使用原生圆钮并相对输入区 max-w-4xl 定位,避免 AntD 按钮宽条问题 - 同步 lucide-react 依赖及流式代码块、全局样式等小改动 Made-with: Cursor
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"axios": "^1.15.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"katex": "^0.16.45",
|
||||
"lucide-react": "^1.9.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { Button, message } from "antd";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
@@ -47,7 +47,7 @@ function MarkdownPreBlock(props: { children: ReactNode }) {
|
||||
<Button
|
||||
type="text"
|
||||
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!"
|
||||
onClick={() => {
|
||||
void onCopy();
|
||||
|
||||
@@ -83,3 +83,9 @@ pre code.hljs {
|
||||
.ant-dropdown-menu-item-danger {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* 修正 lucide 图标在 AntD Button 中的基线偏移 */
|
||||
.ant-btn .ant-btn-icon > .lucide {
|
||||
display: block;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MobileOutlined,
|
||||
MoreOutlined,
|
||||
PaperClipOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RedoOutlined,
|
||||
SearchOutlined,
|
||||
ShareAltOutlined,
|
||||
SettingOutlined,
|
||||
ThunderboltOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
Ellipsis,
|
||||
LogOut,
|
||||
MessageCirclePlus,
|
||||
Paperclip,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pencil,
|
||||
Pin,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Settings,
|
||||
Share2,
|
||||
Smartphone,
|
||||
Trash2,
|
||||
User,
|
||||
Zap,
|
||||
CircleHelp,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
@@ -88,13 +89,7 @@ function subscribeUserProfile(onChange: () => void) {
|
||||
};
|
||||
}
|
||||
|
||||
const INITIAL_MESSAGES: UiMessage[] = [
|
||||
{
|
||||
id: "init-assistant",
|
||||
role: "assistant",
|
||||
content: "你好,我是 ChatOne 助手,有什么可以帮你?",
|
||||
},
|
||||
];
|
||||
const INITIAL_MESSAGES: UiMessage[] = [];
|
||||
|
||||
const STREAM_TIMEOUT_MS = 90000;
|
||||
const MAX_STREAM_RETRY = 1;
|
||||
@@ -155,7 +150,9 @@ export default function HomePage() {
|
||||
}
|
||||
});
|
||||
const messageListRef = useRef<HTMLDivElement | null>(null);
|
||||
const shouldStickToBottomRef = useRef(true);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||||
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
@@ -200,18 +197,18 @@ export default function HomePage() {
|
||||
const userMenuDividerStyle: CSSProperties = { margin: "10px 0" };
|
||||
|
||||
const userMenuItems: MenuProps["items"] = [
|
||||
{ key: "app", icon: <MobileOutlined />, label: "下载手机应用", style: userMenuItemStyle },
|
||||
{ key: "settings", icon: <SettingOutlined />, label: "系统设置", style: userMenuItemStyle },
|
||||
{ key: "app", icon: <Smartphone size={16} />, label: "下载手机应用", style: userMenuItemStyle },
|
||||
{ key: "settings", icon: <Settings size={16} />, label: "系统设置", style: userMenuItemStyle },
|
||||
{
|
||||
key: "help",
|
||||
icon: <QuestionCircleOutlined />,
|
||||
icon: <CircleHelp size={16} />,
|
||||
label: "帮助与反馈",
|
||||
style: userMenuItemStyle,
|
||||
},
|
||||
{ type: "divider", style: userMenuDividerStyle },
|
||||
{
|
||||
key: "logout",
|
||||
icon: <LogoutOutlined />,
|
||||
icon: <LogOut size={16} />,
|
||||
label: "退出登录",
|
||||
danger: true,
|
||||
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(() => {
|
||||
const el = messageListRef.current;
|
||||
if (!el) return;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}, [messages]);
|
||||
const onScroll = () => {
|
||||
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[]> => {
|
||||
if (!window.localStorage.getItem(ACCESS_TOKEN_KEY)) return [];
|
||||
@@ -567,7 +595,7 @@ export default function HomePage() {
|
||||
loading={sessionMutationBusy}
|
||||
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!"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<MessageCirclePlus size={14} strokeWidth={2} />}
|
||||
onClick={() => {
|
||||
void handleNewSession();
|
||||
}}
|
||||
@@ -600,9 +628,9 @@ export default function HomePage() {
|
||||
const active = sid === activeSessionId;
|
||||
const itemMenu: MenuProps = {
|
||||
items: [
|
||||
{ key: "rename", icon: <EditOutlined />, label: "重命名" },
|
||||
{ key: "pin", icon: <VerticalAlignTopOutlined />, label: "置顶" },
|
||||
{ key: "delete", icon: <DeleteOutlined />, danger: true, label: "删除" },
|
||||
{ key: "rename", icon: <Pencil size={16} />, label: "重命名" },
|
||||
{ key: "pin", icon: <Pin size={16} />, label: "置顶" },
|
||||
{ key: "delete", icon: <Trash2 size={16} />, danger: true, label: "删除" },
|
||||
],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
@@ -614,7 +642,7 @@ export default function HomePage() {
|
||||
return (
|
||||
<div
|
||||
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={{
|
||||
background: active ? "var(--ds-active-item)" : undefined,
|
||||
color: active ? "var(--ds-accent)" : undefined,
|
||||
@@ -625,23 +653,29 @@ export default function HomePage() {
|
||||
onClick={() => {
|
||||
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>
|
||||
{(!collapsed || isMobile) && (
|
||||
<Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MoreOutlined />}
|
||||
loading={sessionActionBusyId === sid}
|
||||
className="text-neutral-500 hover:bg-black/5!"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
<div className={`${active ? "flex" : "hidden group-hover:flex"}`}>
|
||||
<Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Ellipsis size={16} />}
|
||||
loading={sessionActionBusyId === sid}
|
||||
className="!flex !h-8 !w-8 !items-center !justify-center rounded-xl text-neutral-500 hover:bg-black/5!"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -650,7 +684,7 @@ export default function HomePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-[var(--ds-border)] p-3">
|
||||
<div className="shrink-0 p-3">
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems, onClick: onUserMenuClick }}
|
||||
placement="topLeft"
|
||||
@@ -658,14 +692,14 @@ export default function HomePage() {
|
||||
>
|
||||
<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" : ""
|
||||
}`}
|
||||
>
|
||||
<Avatar
|
||||
size={36}
|
||||
src={userProfile.avatar}
|
||||
icon={!userProfile.avatar ? <UserOutlined /> : undefined}
|
||||
icon={!userProfile.avatar ? <User size={18} /> : undefined}
|
||||
className="shrink-0 bg-neutral-300"
|
||||
/>
|
||||
{(!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">
|
||||
{userProfile.displayName}
|
||||
</span>
|
||||
<MoreOutlined className="shrink-0 text-neutral-500" />
|
||||
<Ellipsis size={16} className="shrink-0 text-neutral-500" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -696,10 +730,10 @@ export default function HomePage() {
|
||||
width={260}
|
||||
collapsedWidth={72}
|
||||
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-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
|
||||
src="/logo.png"
|
||||
alt=""
|
||||
@@ -724,7 +758,7 @@ export default function HomePage() {
|
||||
style={{ background: "var(--ds-bg-main)" }}
|
||||
>
|
||||
<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" }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -733,11 +767,11 @@ export default function HomePage() {
|
||||
className="text-neutral-500 hover:bg-black/5!"
|
||||
icon={
|
||||
isMobile ? (
|
||||
<MenuUnfoldOutlined />
|
||||
<PanelLeftOpen size={16} />
|
||||
) : collapsed ? (
|
||||
<MenuUnfoldOutlined />
|
||||
<PanelLeftOpen size={16} />
|
||||
) : (
|
||||
<MenuFoldOutlined />
|
||||
<PanelLeftClose size={16} />
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
@@ -770,203 +804,223 @@ export default function HomePage() {
|
||||
</Header>
|
||||
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-5">
|
||||
{messages.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
{!activeSessionId && messages.length === 0 ? (
|
||||
<p className="px-4 text-[18px] text-neutral-800">你好,我是 ChatOne</p>
|
||||
) : (
|
||||
messages.map((item) => (
|
||||
<div
|
||||
className={
|
||||
item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2"
|
||||
}
|
||||
key={item.id}
|
||||
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
{item.role === "user" && (
|
||||
<div
|
||||
className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
|
||||
style={{ background: "var(--ds-user-bubble)" }}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
)}
|
||||
{item.role === "assistant" && item.thinking && (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
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
|
||||
className={
|
||||
item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2"
|
||||
}
|
||||
>
|
||||
{item.role === "user" && (
|
||||
<div
|
||||
className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
|
||||
style={{ background: "var(--ds-user-bubble)" }}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
))}
|
||||
{item.role === "assistant" && item.content && (
|
||||
<>
|
||||
<p className="mt-3 px-4 text-[12px] text-neutral-500 italic">
|
||||
本回答由 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 title="复制">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
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={<RedoOutlined />}
|
||||
className="!px-1 text-neutral-500 hover:bg-black/5!"
|
||||
aria-label="重新生成"
|
||||
onClick={() => {
|
||||
message.info("重新生成功能开发中");
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="分享">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ShareAltOutlined />}
|
||||
className="!px-1 text-neutral-500 hover:bg-black/5!"
|
||||
aria-label="分享"
|
||||
onClick={() => {
|
||||
message.info("分享功能开发中");
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{item.role === "assistant" && item.thinking && (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
size="small"
|
||||
className="!mb-2 !rounded-xl !bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: "think",
|
||||
label: (
|
||||
<span className="text-[13px] text-neutral-600">
|
||||
<Zap size={14} 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>
|
||||
))}
|
||||
{item.role === "assistant" && item.content && (
|
||||
<>
|
||||
<p className="mt-3 px-4 text-[12px] text-neutral-500 italic">
|
||||
本回答由 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 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>
|
||||
</>
|
||||
)}
|
||||
{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 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="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 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"
|
||||
}`}
|
||||
>
|
||||
<ThunderboltOutlined />
|
||||
深度思考
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSmartSearch((v) => !v)}
|
||||
className={`inline-flex 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"
|
||||
}`}
|
||||
>
|
||||
<SearchOutlined />
|
||||
智能搜索
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PaperClipOutlined className="text-neutral-400" />}
|
||||
className="text-neutral-400"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
icon={<ArrowUpOutlined />}
|
||||
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 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={() => {
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
scrollMessageListToBottom("smooth");
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -2932,6 +2932,11 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.30.21"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
|
||||
|
||||
Reference in New Issue
Block a user