feat: add versioned research templates and alpha variants

This commit is contained in:
yuxuanhui
2026-09-08 21:29:21 +08:00
parent b604e6050e
commit f89ae211d2
55 changed files with 5650 additions and 435 deletions
+145 -119
View File
@@ -1,22 +1,30 @@
import { useCallback, useEffect, useState } from "react";
import {
Avatar,
Badge,
Banner,
Button,
Input,
LocaleProvider,
Spin,
SideSheet,
Toast,
Tooltip,
} from "@douyinfe/semi-ui-19";
import zhCN from "@douyinfe/semi-ui-19/lib/es/locale/source/zh_CN";
import { api, post } from "./api";
import { IconSetting, IconComment, IconHistory } from "@douyinfe/semi-icons";
import { ResearchWorkspace } from "./research/ResearchWorkspace";
import { OperatorsPage } from "./research/OperatorsPage";
import { AppSidebar } from "./components/AppSidebar";
import "./components/sidebar.css";
import { api, patch, post } from "./api";
import type { Account, Job } from "./types";
import { AccountPage } from "./pages/AccountPage";
import { DatasetPage } from "./pages/DatasetPage";
import { AlphaPage } from "./pages/AlphaPage";
import { JobPanel } from "./components/JobPanel";
import { BacktestPage } from "./backtests/BacktestPage";
import { WorkspacePreferences } from "./components/WorkspacePreferences";
import { ModelSettingsPanel } from "./ai/ModelSettingsPanel";
import { ChatPanel } from "./ai/ChatPanel";
import { actionDestination, pageFromHash } from "./ai/workspace";
import type { WorkspacePage } from "./ai/workspace";
@@ -25,6 +33,7 @@ import type { PageContext, Resource, UIAction } from "./ai/types";
export default function App() {
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
const [account, setAccount] = useState<Account | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [jobs, setJobs] = useState<Job[]>([]);
const [page, setPage] = useState<WorkspacePage>(() =>
pageFromHash(location.hash),
@@ -35,8 +44,22 @@ export default function App() {
useEffect(() => {
if (page === "backtests") setVisitedBacktests(true);
}, [page]);
const [visitedResearch, setVisitedResearch] = useState<string[]>([]);
const [researchContext, setResearchContext] = useState<PageContext>({
page: "templates",
});
useEffect(() => {
if (["operators", "templates", "variants"].includes(page))
setVisitedResearch((old) => (old.includes(page) ? old : [...old, page]));
}, [page]);
const [catalogModal, setCatalogModal] = useState(false);
const [showJobs, setShowJobs] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const openSettings = () => {
setShowJobs(false);
setChatOpen(false);
setSettingsOpen(true);
};
const [refreshKey, setRefreshKey] = useState(0);
const [resourceVersions, setResourceVersions] = useState<
Record<Resource, number>
@@ -102,6 +125,7 @@ export default function App() {
.then(() => setAuthenticated(true))
.catch(() => setAuthenticated(false));
const expired = () => {
setSettingsOpen(false);
setAuthenticated(false);
setAccount(null);
setJobs([]);
@@ -171,6 +195,7 @@ export default function App() {
const logout = async () => {
try {
await post("/auth/logout");
setSettingsOpen(false);
setAuthenticated(false);
setChatOpen(false);
setShowJobs(false);
@@ -199,110 +224,85 @@ export default function App() {
}}
/>
) : (
<div className="workspace">
<aside
className="sidebar"
inert={(chatOpen && viewport < 1440) || catalogModal}
aria-hidden={catalogModal || undefined}
<div
className={`workspace ${sidebarCollapsed ? "sidebar-collapsed" : ""}`}
>
<AppSidebar
account={account}
page={page}
collapsed={sidebarCollapsed}
blocked={
(chatOpen && viewport < 1440) || catalogModal || settingsOpen
}
onCollapse={() => setSidebarCollapsed((value) => !value)}
onNavigate={changePage}
onLogout={() => void logout()}
onTheme={() => {
if (!account) return;
void patch("/account/preferences", {
display_name: account.display_name,
timezone: account.timezone,
page_size: account.page_size,
theme: account.theme === "dark" ? "light" : "dark",
})
.then(() => refresh())
.catch((error: Error) => Toast.error(error.message));
}}
/>
<div
className="workspace-tools"
hidden={settingsOpen}
inert={
(chatOpen && viewport < 1440) || catalogModal || settingsOpen
}
role="toolbar"
aria-label="工作空间工具"
>
<div className="brand">
<span className="brand-mark">α</span>
<div>Alpha 研究</div>
</div>
<button
aria-label="Alpha 管理"
className={`nav-item ${page === "alphas" ? "active" : ""}`}
aria-current={page === "alphas" ? "page" : undefined}
onClick={() => changePage("alphas")}
<Tooltip content="配置" visible={settingsOpen ? false : undefined}>
<Button
aria-label="配置"
icon={<IconSetting />}
type="tertiary"
theme="borderless"
onClick={openSettings}
/>
</Tooltip>
<Tooltip
content="Bot · 研究助手"
visible={chatOpen ? false : undefined}
>
Alpha 管理
</button>
<button
aria-label="数据集"
className={`nav-item ${page === "datasets" ? "active" : ""}`}
aria-current={page === "datasets" ? "page" : undefined}
onClick={() => changePage("datasets")}
>
数据集
</button>
<button
aria-label="个人信息"
className={`nav-item ${page === "account" ? "active" : ""}`}
aria-current={page === "account" ? "page" : undefined}
onClick={() => changePage("account")}
>
个人信息
</button>
<button
aria-label="回测研究"
className={`nav-item ${page === "backtests" ? "active" : ""}`}
aria-current={page === "backtests" ? "page" : undefined}
onClick={() => changePage("backtests")}
>
回测研究
</button>
<div className="sidebar-bottom">
<div className="connection-line">
<i
className={
account?.connection_status === "connected"
? "connected"
: ""
}
<Button
aria-label="切换研究助手"
aria-expanded={chatOpen}
aria-controls="research-assistant"
icon={<IconComment />}
type="tertiary"
theme={chatOpen ? "light" : "borderless"}
onClick={() => setChatOpen(!chatOpen)}
/>
</Tooltip>
<Tooltip content="同步任务" visible={showJobs ? false : undefined}>
<Badge count={pending || undefined}>
<Button
aria-label="同步任务"
icon={<IconHistory />}
type="tertiary"
theme="borderless"
onClick={() => {
focusBusiness();
setShowJobs(true);
}}
/>
{account?.connection_status === "connected"
? "WorldQuant 已连接"
: "WorldQuant 未连接"}
</div>
</div>
</aside>
</Badge>
</Tooltip>
</div>
<div
className="main-shell"
inert={(chatOpen && viewport < 1440) || catalogModal}
inert={
(chatOpen && viewport < 1440) || catalogModal || settingsOpen
}
aria-hidden={catalogModal || undefined}
>
<header className="topbar">
<div className="breadcrumbs">研究工作空间</div>
<div className="top-actions">
<Badge count={pending}>
<Button
type="tertiary"
theme="borderless"
onClick={() => {
focusBusiness();
setShowJobs(true);
}}
>
同步任务
</Button>
</Badge>
<Button
aria-label="切换研究助手"
aria-expanded={chatOpen}
aria-controls="research-assistant"
type="tertiary"
theme={chatOpen ? "light" : "borderless"}
onClick={() => setChatOpen(!chatOpen)}
>
AI 助手
</Button>
<div className="top-divider" />
<Avatar size="small" color="grey">
{account?.display_name.slice(0, 1) || "研"}
</Avatar>
<span className="account-name">
{account?.display_name ?? "研究员"}
</span>
<Button
type="tertiary"
aria-label="退出登录"
theme="borderless"
onClick={() => void logout()}
>
退出
</Button>
</div>
</header>
<main
className={`page-content ${page !== "account" ? "bounded-page" : "account-page"}`}
>
@@ -370,8 +370,44 @@ export default function App() {
/>
)}
</div>
{visitedResearch.includes("operators") && (
<div className="alpha-page-view" hidden={page !== "operators"}>
<OperatorsPage active={page === "operators"} />
</div>
)}
{(["templates", "variants"] as const).map(
(researchPage) =>
visitedResearch.includes(researchPage) && (
<div
className="alpha-page-view"
key={researchPage}
hidden={page !== researchPage}
>
<ResearchWorkspace
page={researchPage}
active={page === researchPage}
action={aiAction}
onAction={handleAction}
onContext={setResearchContext}
/>
</div>
),
)}
</main>
</div>
<SideSheet
title="配置"
className="settings-drawer"
placement="right"
visible={settingsOpen}
width={Math.min(560, viewport)}
keepDOM
closeOnEsc
onCancel={() => setSettingsOpen(false)}
>
<ModelSettingsPanel />
<WorkspacePreferences account={account} onChange={actionDone} />
</SideSheet>
<JobPanel
visible={showJobs && !(viewport < 1440 && chatOpen)}
chatOffset={chatOffset}
@@ -384,17 +420,6 @@ export default function App() {
changePage("account");
}}
/>
{!chatOpen && !catalogModal && (
<Button
className="ai-launcher"
aria-label="打开研究助手"
theme="light"
type="tertiary"
onClick={() => setChatOpen(true)}
>
AI 研究助手
</Button>
)}
{chatOpen && viewport < 1440 && (
<div
className="ai-mask"
@@ -412,6 +437,15 @@ export default function App() {
onClose={() => setChatOpen(false)}
context={
{
operators: { page: "operators" as const },
templates:
researchContext.page === "templates"
? researchContext
: { page: "templates" as const },
variants:
researchContext.page === "variants"
? researchContext
: { page: "variants" as const },
alphas: alphaContext,
datasets: datasetContext,
backtests: backtestContext,
@@ -419,15 +453,7 @@ export default function App() {
}[page]
}
timezone={account?.timezone}
onSettings={() => {
focusBusiness();
changePage("account");
requestAnimationFrame(() =>
document
.getElementById("model-settings")
?.scrollIntoView({ block: "start" }),
);
}}
onSettings={openSettings}
onChanged={aiChanged}
onAction={handleAction}
/>