补齐会话重命名/删除与会话菜单能力,新增聊天参数透传与本地开关持久化,并统一输入区、消息区、代码块和弹框等关键交互样式。 Made-with: Cursor
This commit is contained in:
@@ -2,18 +2,23 @@ import type { CSSProperties } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MessageOutlined,
|
||||
MobileOutlined,
|
||||
MoreOutlined,
|
||||
PaperClipOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RedoOutlined,
|
||||
SearchOutlined,
|
||||
ShareAltOutlined,
|
||||
SettingOutlined,
|
||||
ThunderboltOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
@@ -24,7 +29,9 @@ import {
|
||||
Dropdown,
|
||||
Input,
|
||||
Layout,
|
||||
Modal,
|
||||
Select,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
@@ -33,11 +40,13 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { ACCESS_TOKEN_KEY, SessionExpiredError, USER_KEY, logout } from "../api/auth";
|
||||
import {
|
||||
createChatSession,
|
||||
deleteChatSession,
|
||||
listChatSessionMessages,
|
||||
listChatSessions,
|
||||
normalizeSessionMessages,
|
||||
sessionDisplayTitle,
|
||||
sessionStableId,
|
||||
updateChatSessionTitle,
|
||||
type ChatSession,
|
||||
type ChatTurnForUi,
|
||||
} from "../api/chatSessions";
|
||||
@@ -49,22 +58,18 @@ const MOBILE_WIDTH = 750;
|
||||
|
||||
type UiMessage = ChatTurnForUi;
|
||||
|
||||
type StoredUserProfile = {
|
||||
nickname: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
|
||||
function parseStoredUserProfileJson(raw: string | null): { displayName: string; avatar?: string } {
|
||||
try {
|
||||
if (!raw) return { displayName: "用户" };
|
||||
const u = JSON.parse(raw) as Record<string, unknown>;
|
||||
const nickname = typeof u.nickname === "string" && u.nickname.trim() ? u.nickname.trim() : "";
|
||||
const displayName =
|
||||
nickname ||
|
||||
(typeof u.name === "string" && u.name) ||
|
||||
(typeof u.username === "string" && u.username) ||
|
||||
(typeof u.phone === "string" && u.phone) ||
|
||||
"用户";
|
||||
const avatar =
|
||||
(typeof u.avatar === "string" && u.avatar) ||
|
||||
(typeof u.avatarUrl === "string" && u.avatarUrl) ||
|
||||
undefined;
|
||||
return { displayName: String(displayName), avatar };
|
||||
const user = JSON.parse(raw) as StoredUserProfile;
|
||||
const displayName = user.nickname.trim() || "用户";
|
||||
const avatar = user.avatarUrl || undefined;
|
||||
return { displayName, avatar };
|
||||
} catch {
|
||||
return { displayName: "用户" };
|
||||
}
|
||||
@@ -94,35 +99,70 @@ const INITIAL_MESSAGES: UiMessage[] = [
|
||||
const STREAM_TIMEOUT_MS = 90000;
|
||||
const MAX_STREAM_RETRY = 1;
|
||||
const CONTEXT_WINDOW_SIZE = 8;
|
||||
const DEEP_THINK_STORAGE_KEY = "chatone-deep-think-enabled";
|
||||
const SMART_SEARCH_STORAGE_KEY = "chatone-smart-search-enabled";
|
||||
const SELECTED_MODEL_STORAGE_KEY = "chatone-selected-model";
|
||||
|
||||
/** 展示名与请求 model 字段;需与后端实际支持的模型 id 一致 */
|
||||
const DEFAULT_QWEN_MODEL = "qwen3.5-flash";
|
||||
const DEFAULT_QWEN_MODEL = "qwen3.6-plus";
|
||||
const QWEN_MODEL_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "qwen3-max", label: "Qwen3-Max" },
|
||||
{ value: "qwen3.6-plus", label: "Qwen3.6-Plus" },
|
||||
{ value: DEFAULT_QWEN_MODEL, label: "Qwen3.5-Flash" },
|
||||
{ value: DEFAULT_QWEN_MODEL, label: "Qwen3.6-Plus" },
|
||||
{ value: "qwen3.5-flash", label: "Qwen3.5-Flash" },
|
||||
{ value: "qwen-plus", label: "Qwen-Plus" },
|
||||
{ value: "deepseek-r1", label: "DeepSeek-R1" },
|
||||
{ value: "kimi-k2.6", label: "Kimi-K2.6" },
|
||||
];
|
||||
|
||||
function replaceSessionUrl(sessionId: string) {
|
||||
const base = import.meta.env.BASE_URL ?? "/";
|
||||
const prefix = base === "/" ? "" : base.replace(/\/$/, "");
|
||||
const nextPath = `${prefix}/s/${encodeURIComponent(sessionId)}`;
|
||||
window.history.replaceState(window.history.state, "", nextPath);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const navigate = useNavigate();
|
||||
const { sessionId: routeSessionId } = useParams<{ sessionId?: string }>();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [viewportWidth, setViewportWidth] = useState(0);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [messages, setMessages] = useState<UiMessage[]>(INITIAL_MESSAGES);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [deepThink, setDeepThink] = useState(false);
|
||||
const [smartSearch, setSmartSearch] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState(DEFAULT_QWEN_MODEL);
|
||||
const [deepThink, setDeepThink] = useState(() => {
|
||||
try {
|
||||
return window.localStorage.getItem(DEEP_THINK_STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [smartSearch, setSmartSearch] = useState(() => {
|
||||
try {
|
||||
return window.localStorage.getItem(SMART_SEARCH_STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [selectedModel, setSelectedModel] = useState(() => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(SELECTED_MODEL_STORAGE_KEY);
|
||||
const supported = QWEN_MODEL_OPTIONS.some((item) => item.value === raw);
|
||||
return supported && raw ? raw : DEFAULT_QWEN_MODEL;
|
||||
} catch {
|
||||
return DEFAULT_QWEN_MODEL;
|
||||
}
|
||||
});
|
||||
const messageListRef = useRef<HTMLDivElement | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
const [sessionMutationBusy, setSessionMutationBusy] = useState(false);
|
||||
const [sessionActionBusyId, setSessionActionBusyId] = useState<string | null>(null);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const activeSessionIdRef = useRef<string | null>(null);
|
||||
|
||||
const userJsonSnapshot = useSyncExternalStore(
|
||||
subscribeUserProfile,
|
||||
@@ -134,6 +174,22 @@ export default function HomePage() {
|
||||
[userJsonSnapshot],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
activeSessionIdRef.current = activeSessionId;
|
||||
}, [activeSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(DEEP_THINK_STORAGE_KEY, deepThink ? "1" : "0");
|
||||
}, [deepThink]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(SMART_SEARCH_STORAGE_KEY, smartSearch ? "1" : "0");
|
||||
}, [smartSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(SELECTED_MODEL_STORAGE_KEY, selectedModel);
|
||||
}, [selectedModel]);
|
||||
|
||||
/** 用户信息下拉:略增大行高与项间距 */
|
||||
const userMenuItemStyle: CSSProperties = {
|
||||
height: "auto",
|
||||
@@ -189,7 +245,6 @@ export default function HomePage() {
|
||||
window.innerWidth,
|
||||
document.documentElement.clientWidth || window.innerWidth,
|
||||
);
|
||||
setViewportWidth(w);
|
||||
const mobile = w < MOBILE_WIDTH;
|
||||
setIsMobile(mobile);
|
||||
if (!mobile) setMobileSidebarOpen(false);
|
||||
@@ -229,6 +284,10 @@ export default function HomePage() {
|
||||
void refreshSessionList();
|
||||
}, [refreshSessionList]);
|
||||
|
||||
/**
|
||||
* 打开指定会话并加载历史消息。
|
||||
* 默认会把当前会话 id 同步到路由;当由路由参数驱动时可关闭 URL 更新,避免重复跳转。
|
||||
*/
|
||||
const openSession = useCallback(
|
||||
async (sessionId: string, options?: { updateUrl?: boolean }) => {
|
||||
if (options?.updateUrl ?? true) {
|
||||
@@ -253,32 +312,140 @@ export default function HomePage() {
|
||||
setMessages(INITIAL_MESSAGES.map((m) => ({ ...m })));
|
||||
return;
|
||||
}
|
||||
// 路由已与当前会话一致时,不重复加载,避免覆盖流式中的本地消息状态。
|
||||
if (routeSessionId === activeSessionIdRef.current) return;
|
||||
void openSession(routeSessionId, { updateUrl: false });
|
||||
}, [openSession, routeSessionId]);
|
||||
|
||||
const handleNewSession = async () => {
|
||||
if (sessionMutationBusy) return;
|
||||
setSessionMutationBusy(true);
|
||||
try {
|
||||
const row = await createChatSession();
|
||||
await refreshSessionList();
|
||||
setActiveSessionId(row.id);
|
||||
setMessages(INITIAL_MESSAGES.map((m) => ({ ...m })));
|
||||
navigate(`/s/${encodeURIComponent(row.id)}`);
|
||||
} catch (e) {
|
||||
const text = e instanceof Error ? e.message : "新建会话失败";
|
||||
message.error(text);
|
||||
} finally {
|
||||
setSessionMutationBusy(false);
|
||||
}
|
||||
setActiveSessionId(null);
|
||||
setMessages(INITIAL_MESSAGES.map((m) => ({ ...m })));
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const handleRenameSession = useCallback(
|
||||
(session: ChatSession) => {
|
||||
const sid = sessionStableId(session);
|
||||
if (!sid || sessionActionBusyId) return;
|
||||
let nextTitle = session.title;
|
||||
modal.confirm({
|
||||
centered: true,
|
||||
title: "重命名会话",
|
||||
content: (
|
||||
<Input
|
||||
autoFocus
|
||||
maxLength={200}
|
||||
defaultValue={session.title}
|
||||
placeholder="请输入会话标题"
|
||||
onChange={(event) => {
|
||||
nextTitle = event.target.value;
|
||||
}}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
okText: "保存",
|
||||
cancelText: "取消",
|
||||
async onOk() {
|
||||
if (nextTitle === session.title) return;
|
||||
setSessionActionBusyId(sid);
|
||||
try {
|
||||
await updateChatSessionTitle(sid, { title: nextTitle });
|
||||
await refreshSessionList();
|
||||
message.success("会话已重命名");
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : "重命名会话失败";
|
||||
message.error(text);
|
||||
throw error;
|
||||
} finally {
|
||||
setSessionActionBusyId(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[modal, refreshSessionList, sessionActionBusyId],
|
||||
);
|
||||
|
||||
const handleDeleteSession = useCallback(
|
||||
(session: ChatSession) => {
|
||||
const sid = sessionStableId(session);
|
||||
if (!sid || sessionActionBusyId) return;
|
||||
modal.confirm({
|
||||
centered: true,
|
||||
title: "删除会话",
|
||||
content: "确认删除该会话?删除后不可恢复。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
async onOk() {
|
||||
setSessionActionBusyId(sid);
|
||||
try {
|
||||
await deleteChatSession(sid);
|
||||
if (activeSessionId === sid) {
|
||||
abortRef.current?.abort();
|
||||
setIsSending(false);
|
||||
setActiveSessionId(null);
|
||||
setMessages(INITIAL_MESSAGES.map((m) => ({ ...m })));
|
||||
navigate("/");
|
||||
}
|
||||
await refreshSessionList();
|
||||
message.success("会话已删除");
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : "删除会话失败";
|
||||
message.error(text);
|
||||
throw error;
|
||||
} finally {
|
||||
setSessionActionBusyId(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[activeSessionId, modal, navigate, refreshSessionList, sessionActionBusyId],
|
||||
);
|
||||
|
||||
const handleCopyUserMessage = useCallback(async (content: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
message.success("已复制");
|
||||
} catch {
|
||||
message.error("复制失败,请检查浏览器权限");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCopyAssistantMessage = useCallback(async (content: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
message.success("已复制");
|
||||
} catch {
|
||||
message.error("复制失败,请检查浏览器权限");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendMessage = async () => {
|
||||
const trimmed = inputValue.trim();
|
||||
if (!trimmed || isSending) return;
|
||||
if (!activeSessionId) {
|
||||
message.warning("请先新建会话");
|
||||
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 : "新建会话失败";
|
||||
message.error(text);
|
||||
return;
|
||||
} finally {
|
||||
setSessionMutationBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue("");
|
||||
@@ -330,7 +497,9 @@ export default function HomePage() {
|
||||
await streamQwenChat({
|
||||
messages: retryMessages,
|
||||
model: selectedModel,
|
||||
sessionId: activeSessionId ?? undefined,
|
||||
sessionId: targetSessionId ?? undefined,
|
||||
enableWebSearch: smartSearch,
|
||||
enableThinking: deepThink,
|
||||
signal: controller.signal,
|
||||
timeoutMs: STREAM_TIMEOUT_MS,
|
||||
onToken: (token) => {
|
||||
@@ -429,21 +598,52 @@ export default function HomePage() {
|
||||
if (!sid) return null;
|
||||
const title = sessionDisplayTitle(s);
|
||||
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: "删除" },
|
||||
],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
if (key === "rename") void handleRenameSession(s);
|
||||
if (key === "pin") message.info("置顶功能开发中");
|
||||
if (key === "delete") void handleDeleteSession(s);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={sid}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void openSession(sid);
|
||||
}}
|
||||
className="mb-1 flex w-full items-center gap-2 rounded-xl px-3 py-2.5 text-left text-[13px] text-neutral-700 transition-colors hover:bg-black/5"
|
||||
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"
|
||||
style={{
|
||||
background: active ? "var(--ds-active-item)" : undefined,
|
||||
color: active ? "var(--ds-accent)" : undefined,
|
||||
}}
|
||||
>
|
||||
<MessageOutlined className="shrink-0 text-neutral-400" />
|
||||
{(!collapsed || isMobile) && <span className="truncate">{title}</span>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void openSession(sid);
|
||||
}}
|
||||
className="flex min-h-12 min-w-0 flex-1 items-center rounded-xl px-3 py-3 text-left"
|
||||
>
|
||||
{(!collapsed || isMobile) && <span className="truncate">{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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -487,6 +687,7 @@ export default function HomePage() {
|
||||
className="chat-one-shell flex h-dvh max-h-dvh overflow-hidden"
|
||||
style={{ background: "var(--ds-bg-main)" }}
|
||||
>
|
||||
{modalContextHolder}
|
||||
{!isMobile && (
|
||||
<Sider
|
||||
trigger={null}
|
||||
@@ -574,7 +775,7 @@ export default function HomePage() {
|
||||
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-3xl flex-col gap-5">
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-5">
|
||||
{messages.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
@@ -582,24 +783,22 @@ export default function HomePage() {
|
||||
>
|
||||
<div
|
||||
className={
|
||||
item.role === "user"
|
||||
? "max-w-[85%] rounded-2xl border px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
|
||||
: "max-w-[85%] space-y-2"
|
||||
}
|
||||
style={
|
||||
item.role === "user"
|
||||
? {
|
||||
background: "var(--ds-user-bubble)",
|
||||
borderColor: "var(--ds-user-border)",
|
||||
}
|
||||
: undefined
|
||||
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.thinking && (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
size="small"
|
||||
className="!mb-2 !rounded-xl !bg-neutral-50 !border !border-[var(--ds-border)]"
|
||||
className="!mb-2 !rounded-xl !bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: "think",
|
||||
@@ -622,27 +821,92 @@ export default function HomePage() {
|
||||
(item.content ? (
|
||||
<StreamMessage content={item.content} />
|
||||
) : (
|
||||
<div className="rounded-2xl border border-[var(--ds-border)] bg-neutral-50/80 px-4 py-3 text-[14px] text-neutral-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>正在回复</span>
|
||||
<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 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 === "user" && item.content}
|
||||
{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>
|
||||
</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 className="shrink-0 bg-[var(--ds-bg-main)] px-4 pb-4 pt-0 md:px-8 md:pb-4 md:pt-0">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="rounded-2xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm">
|
||||
<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)}
|
||||
@@ -653,7 +917,7 @@ export default function HomePage() {
|
||||
}}
|
||||
placeholder="给 ChatOne 发送消息"
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 1, maxRows: 6 }}
|
||||
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">
|
||||
@@ -706,11 +970,9 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{import.meta.env.DEV && (
|
||||
<Typography.Text type="secondary" className="mt-2 block text-center text-[11px]">
|
||||
W:{viewportWidth}px · {isMobile ? "mobile" : "desktop"}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<p className="m-0 py-2 text-center text-[11px] text-neutral-500">
|
||||
内容由 AI 生成,请注意核实
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user