import { useCallback, useEffect, useState } from "react"; import { 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 { IconSetting, IconAIFilledLevel3, IconHistory, } from "@douyinfe/semi-icons"; import { QuantFlowPage } from "./research/QuantFlowPage"; import { PipelinePage } from "./research/PipelinePage"; import { FeaturesPage } from "./research/FeaturesPage"; 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 { MCPKeysPage } from "./pages/MCPKeysPage"; import { AccountPage } from "./pages/AccountPage"; import { DatasetPage } from "./pages/DatasetPage"; import { AlphaPage } from "./pages/AlphaPage"; import { HomePage } from "./pages/HomePage"; 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"; import type { PageContext, Resource, UIAction } from "./ai/types"; export default function App() { const [authenticated, setAuthenticated] = useState(null); const [account, setAccount] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [jobs, setJobs] = useState([]); const [page, setPage] = useState(() => pageFromHash(location.hash), ); const [visitedBacktests, setVisitedBacktests] = useState( page === "backtests", ); useEffect(() => { if (page === "backtests") setVisitedBacktests(true); }, [page]); const [visitedResearch, setVisitedResearch] = useState([]); const [researchContext, setResearchContext] = useState({ page: "templates", }); useEffect(() => { if ( [ "operators", "templates", "variants", "features", "pipeline", "quantflow", ].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 >({ alphas: 0, datasets: 0, backtests: 0, jobs: 0, account: 0, }); const [pollError, setPollError] = useState(""); const [chatOpen, setChatOpen] = useState(false); const [chatWidth, setChatWidth] = useState(420); const [viewport, setViewport] = useState(window.innerWidth); const [alphaContext, setAlphaContext] = useState({ page: "alphas", }); const [backtestContext, setBacktestContext] = useState({ page: "backtests", }); const [datasetContext, setDatasetContext] = useState({ page: "datasets", }); const [aiAction, setAIAction] = useState(null); const chatOffset = viewport >= 1440 && chatOpen ? chatWidth : 0; const focusBusiness = useCallback(() => { if (window.innerWidth < 1440) setChatOpen(false); }, []); useEffect(() => { const resize = () => setViewport(window.innerWidth); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []); useEffect(() => { document.documentElement.style.setProperty( "--chat-space", `${chatOffset}px`, ); document.documentElement.style.setProperty( "--chat-width", `${chatWidth}px`, ); return () => { document.documentElement.style.setProperty("--chat-space", "0px"); }; }, [chatOffset, chatWidth]); const refresh = useCallback(async () => { try { const [nextAccount, nextJobs] = await Promise.all([ api("/account"), api("/sync-jobs"), ]); setAccount(nextAccount); setJobs(nextJobs); setPollError(""); } catch (error) { setPollError((error as Error).message); } }, []); useEffect(() => { api("/auth/me") .then(() => setAuthenticated(true)) .catch(() => setAuthenticated(false)); const expired = () => { setSettingsOpen(false); setAuthenticated(false); setAccount(null); setJobs([]); setChatOpen(false); setShowJobs(false); }; window.addEventListener("session-expired", expired); const hash = () => setPage(pageFromHash(location.hash)); window.addEventListener("hashchange", hash); return () => { window.removeEventListener("session-expired", expired); window.removeEventListener("hashchange", hash); }; }, []); useEffect(() => { if (!authenticated) return; void refresh(); const timer = window.setInterval(() => void refresh(), 3000); return () => clearInterval(timer); }, [authenticated, refresh]); useEffect(() => { document.body.setAttribute("theme-mode", account?.theme ?? "light"); }, [account?.theme]); const completedVersion = jobs .filter( (j) => j.status === "completed" || j.status === "completed_with_errors", ) .map((j) => `${j.id}:${j.updated_at}`) .join("|"); const pending = jobs.filter((j) => ["queued", "running", "waiting_auth", "waiting_connection"].includes( j.status, ), ).length; const actionDone = () => { void refresh(); setRefreshKey((k) => k + 1); }; const aiChanged = (resources: Resource[]) => { if (resources.includes("jobs") || resources.includes("account")) void refresh(); setResourceVersions((old) => { const next = { ...old }; for (const resource of resources) if (Object.hasOwn(next, resource)) next[resource] += 1; return next; }); }; const taskCreated = () => { actionDone(); setShowJobs(true); }; const changePage = (next: WorkspacePage) => { location.hash = next; setPage(next); }; const handleAction = (action: UIAction) => { const destination = actionDestination(action); if (!destination) return; if (destination.chat === "open") setChatOpen(true); else if (destination.chat === "close") setChatOpen(false); else focusBusiness(); if (destination.page) changePage(destination.page); setAIAction(action); }; const logout = async () => { try { await post("/auth/logout"); setSettingsOpen(false); setAuthenticated(false); setChatOpen(false); setShowJobs(false); setAccount(null); setJobs([]); setAIAction(null); setAlphaContext({ page: "alphas" }); setDatasetContext({ page: "datasets" }); setBacktestContext({ page: "backtests" }); } catch (e) { Toast.error((e as Error).message); } }; return ( {authenticated === null ? (
) : !authenticated ? ( { setAuthenticated(true); void refresh(); }} /> ) : (
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)); }} />
{page === "home" && ( changePage("account")} /> )} {page === "mcp-keys" && } {pollError && ( )} {visitedResearch.includes("quantflow") && ( )} {visitedResearch.includes("pipeline") && ( )} {visitedResearch.includes("features") && ( )} {visitedResearch.includes("operators") && ( )} {(["templates", "variants"] as const).map( (researchPage) => visitedResearch.includes(researchPage) && ( ), )}
setSettingsOpen(false)} > setShowJobs(false)} onChange={actionDone} onAccount={() => { setShowJobs(false); changePage("account"); }} /> {chatOpen && viewport < 1440 && ( )} ); } function Login({ onLogin }: { onLogin: () => void }) { const [username, setUsername] = useState("admin"); const [password, setPassword] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); async function submit(event: React.FormEvent) { event.preventDefault(); setBusy(true); setError(""); try { await post("/auth/login", { username, password }); setPassword(""); onLogin(); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } return (
Alpha 研究

登录研究工作空间

使用部署时设置的系统账户登录。

{error && }
WorldQuant 账户在登录后单独连接。
); }