Files
worldquant-alpha-system/frontend/src/components/AppSidebar.tsx
T
yuxuanhui e256d6fef1
Deploy production / deploy (push) Successful in 56s
feat: add Super Alpha research, management and MCP workflows
2026-09-13 12:32:16 +08:00

310 lines
9.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { Avatar, Modal, Popover, Tooltip } from "@douyinfe/semi-ui-19";
import {
IconBeaker,
IconChevronDown,
IconClose,
IconCommand,
IconExit,
IconGridView,
IconList,
IconMoon,
IconSidebar,
IconUser,
} from "@douyinfe/semi-icons";
import type { Account } from "../types";
import type { WorkspacePage } from "../ai/workspace";
const navigation = [
{ id: "home", label: "首页看板", icon: IconGridView, group: "概览" },
{ id: "datasets", label: "数据目录", icon: IconList, group: "数据与素材" },
{ id: "fields", label: "字段目录", icon: IconList, group: "数据与素材" },
{
id: "preparations",
label: "数据准备",
icon: IconList,
group: "数据与素材",
},
{ id: "operators", label: "算子库", icon: IconList, group: "数据与素材" },
{
id: "templates",
label: "模板工坊",
icon: IconGridView,
group: "数据与素材",
},
{ id: "features", label: "特征工程", icon: IconBeaker, group: "研究实验" },
{ id: "variants", label: "Alpha 变体", icon: IconBeaker, group: "研究实验" },
{
id: "superalpha-research",
label: "Super Alpha 研究",
icon: IconBeaker,
group: "研究实验",
},
{ id: "backtests", label: "回测研究", icon: IconBeaker, group: "研究实验" },
{ id: "pipeline", label: "研究流水线", icon: IconBeaker, group: "研究编排" },
{ id: "quantflow", label: "QuantFlow", icon: IconBeaker, group: "研究编排" },
{ id: "alphas", label: "Alpha 管理", icon: IconGridView, group: "研究成果" },
{
id: "superalphas",
label: "Super Alpha 管理",
icon: IconGridView,
group: "研究成果",
},
{ id: "mcp-keys", label: "MCP Key", icon: IconCommand, group: "系统管理" },
{ id: "account", label: "个人信息", icon: IconUser, group: "" },
] as const;
export function AppSidebar({
account,
page,
collapsed,
blocked,
onCollapse,
onNavigate,
onLogout,
onTheme,
}: {
account: Account | null;
page: WorkspacePage;
collapsed: boolean;
blocked: boolean;
onCollapse: () => void;
onNavigate: (page: WorkspacePage) => void;
onLogout: () => void;
onTheme: () => void;
}) {
const [userOpen, setUserOpen] = useState(false);
const [commandOpen, setCommandOpen] = useState(false);
const [recent, setRecent] = useState<WorkspacePage[]>([page]);
const name = account?.display_name || "研究员";
useEffect(() => {
setRecent((old) => [page, ...old.filter((id) => id !== page)].slice(0, 4));
}, [page]);
useEffect(() => {
if (blocked) {
setUserOpen(false);
setCommandOpen(false);
}
const shortcut = (event: KeyboardEvent) => {
if (
!blocked &&
(event.metaKey || event.ctrlKey) &&
event.key.toLowerCase() === "k"
) {
event.preventDefault();
setCommandOpen((open) => !open);
}
};
window.addEventListener("keydown", shortcut);
return () => window.removeEventListener("keydown", shortcut);
}, [blocked]);
function navigate(id: WorkspacePage) {
setUserOpen(false);
setCommandOpen(false);
onNavigate(id);
}
function closeRecent(id: WorkspacePage) {
if (recent.length <= 1) return;
const next = recent.filter((item) => item !== id);
setRecent(next);
if (id === page) navigate(next[0]);
}
return (
<>
<aside
className={`app-sidebar ${collapsed ? "is-collapsed" : ""}`}
inert={blocked}
aria-label="工作空间导航"
>
<div className="sidebar-user-row">
<Popover
trigger="click"
position={collapsed ? "rightTop" : "bottomLeft"}
visible={userOpen}
onVisibleChange={setUserOpen}
showArrow={false}
content={
<div className="sidebar-user-menu">
<div className="sidebar-user-details">
<strong>{name}</strong>
{account?.email && account.email !== name && (
<span>{account.email}</span>
)}
<span>
WorldQuant{" "}
{account?.connection_status === "connected"
? "已连接"
: "未连接"}
</span>
</div>
<button
aria-label="查看个人信息"
onClick={() => navigate("account")}
>
<IconUser />
查看个人信息
</button>
<button
aria-label={
account?.theme === "dark" ? "切换浅色主题" : "切换深色主题"
}
onClick={onTheme}
>
<IconMoon />
{account?.theme === "dark" ? "切换浅色主题" : "切换深色主题"}
</button>
<button
aria-label="退出登录"
onClick={() => {
setUserOpen(false);
onLogout();
}}
>
<IconExit />
退出登录
</button>
</div>
}
>
<button
className="sidebar-user-trigger"
aria-label="用户菜单"
aria-expanded={userOpen}
>
<Avatar size="small" color="grey">
{name.slice(0, 2).toUpperCase()}
</Avatar>
{!collapsed && (
<>
<span className="account-name">{name}</span>
<IconChevronDown size="small" />
</>
)}
</button>
</Popover>
</div>
{!collapsed && (
<div className="sidebar-command">
<button onClick={() => setCommandOpen(true)}>
<IconCommand />
<span>快捷导航</span>
<kbd>⌘K</kbd>
</button>
</div>
)}
<nav className="sidebar-nav" aria-label="主导航">
{[
"概览",
"数据与素材",
"研究实验",
"研究编排",
"研究成果",
"系统管理",
].map((group) => (
<div key={group}>
{!collapsed && (
<div className="sidebar-group-heading">{group}</div>
)}
{navigation
.filter((item) => item.group === group)
.map(({ id, label, icon: Icon }) => (
<Tooltip
key={id}
content={label}
position="right"
visible={collapsed ? undefined : false}
>
<button
className={`sidebar-nav-item ${id === page ? "active" : ""}`}
aria-label={label}
aria-current={id === page ? "page" : undefined}
onClick={() => navigate(id)}
>
<Icon style={{ fontSize: 17 }} />
{!collapsed && <span>{label}</span>}
</button>
</Tooltip>
))}
</div>
))}
</nav>
<div className="sidebar-footer">
{!collapsed && (
<div className="recent-heading">
<span>最近访问</span>
<button onClick={() => setRecent([page])}>全部关闭</button>
</div>
)}
<nav aria-label="最近访问">
{recent.map((id) => {
const item = navigation.find((item) => item.id === id)!;
const Icon = item.icon;
return (
<div
key={id}
className={`recent-item ${id === page ? "active" : ""}`}
>
<Tooltip
content={item.label}
position="right"
visible={collapsed ? undefined : false}
>
<button
className="recent-link"
aria-label={`最近访问:${item.label}`}
onClick={() => navigate(id)}
>
<Icon style={{ fontSize: 15 }} />
{!collapsed && <span>{item.label}</span>}
</button>
</Tooltip>
{!collapsed && (
<button
className="recent-close"
aria-label={`关闭最近访问:${item.label}`}
disabled={recent.length === 1}
onClick={() => closeRecent(id)}
>
<IconClose size="small" />
</button>
)}
</div>
);
})}
</nav>
<div className="sidebar-collapse">
<Tooltip
content={collapsed ? "展开导航" : "收起导航"}
position="right"
>
<button
onClick={onCollapse}
aria-label={collapsed ? "展开导航" : "收起导航"}
>
<IconSidebar />
{!collapsed && <span>收起导航</span>}
</button>
</Tooltip>
</div>
</div>
</aside>
<Modal
title="快捷导航"
visible={commandOpen}
footer={null}
onCancel={() => setCommandOpen(false)}
width={420}
>
<div className="command-items">
{navigation.map(({ id, label, icon: Icon }) => (
<button key={id} aria-label={label} onClick={() => navigate(id)}>
<Icon />
{label}
</button>
))}
</div>
</Modal>
</>
);
}