feat: add versioned research templates and alpha variants
This commit is contained in:
+145
-119
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user