Files
worldquant-alpha-system/frontend/src/research/OperatorsPage.tsx
T

240 lines
7.2 KiB
TypeScript

import { ResearchList, ResearchDetail } from "./ResearchList";
import { ResearchSelect } from "./ResearchSelect";
import { useEffect, useState } from "react";
import {
Banner,
Button,
Checkbox,
Input,
TextArea,
} from "@douyinfe/semi-ui-19";
import { api, patch, post, formatTime } from "../api";
import "./workspace.css";
type Operator = {
name: string;
category: string;
description: string;
definition: string;
example: string;
parameters: unknown;
scope: string[];
local: { note: string; favorite: boolean; version: number };
};
type Results = {
items: Operator[];
total: number;
categories: string[];
fetched_at: string | null;
};
export function OperatorsPage({ active }: { active: boolean }) {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("");
const [favorite, setFavorite] = useState(false);
const [offset, setOffset] = useState(0);
const [result, setResult] = useState<Results>({
items: [],
total: 0,
categories: [],
fetched_at: null,
});
const [selected, setSelected] = useState<Operator | null>(null);
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
async function load() {
setResult(
await api(
`/catalog/operators?${new URLSearchParams({ q: query, category, favorite: String(favorite), limit: "25", offset: String(offset) })}`,
),
);
}
async function task(operation: () => Promise<void>) {
setBusy(true);
setError("");
try {
await operation();
} catch (error) {
setError((error as Error).message);
} finally {
setBusy(false);
}
}
useEffect(() => {
if (active) void task(load);
}, [active, query, category, favorite, offset]);
return (
<div className="research-workspace research-list-page">
{error && !selected && <Banner type="danger" description={error} />}
<ResearchList
resetKey={JSON.stringify([query, category, favorite, offset])}
label="算子库"
filters={
<div className="research-toolbar">
<Input
aria-label="搜索算子"
placeholder="名称、参数或描述"
value={query}
onChange={(value) => {
setQuery(value);
setOffset(0);
}}
/>
<ResearchSelect
label="算子分类"
value={category}
optionList={[
{ value: "", label: "全部分类" },
...result.categories.map((value) => ({ value, label: value })),
]}
onChange={(value) => {
setCategory(value as string);
setOffset(0);
}}
/>
<Checkbox
checked={favorite}
onChange={(event) => {
setFavorite(!!event.target.checked);
setOffset(0);
}}
>
只看收藏
</Checkbox>
</div>
}
actions={
<>
<Button
theme="solid"
loading={busy}
onClick={() =>
void task(async () => {
await post("/catalog/operators/refresh");
await load();
})
}
>
同步算子目录
</Button>
<span className="muted">
同步时间:{formatTime(result.fetched_at)}
</span>
</>
}
pagination={
<div className="research-toolbar">
<span>共 {result.total} 个</span>
<Button
disabled={!offset}
onClick={() => setOffset(Math.max(0, offset - 25))}
>
上一页
</Button>
<Button
disabled={offset + 25 >= result.total}
onClick={() => setOffset(offset + 25)}
>
下一页
</Button>
</div>
}
>
<table className="research-table">
<thead>
<tr>
<th>算子</th>
<th>分类</th>
<th>定义</th>
</tr>
</thead>
<tbody>
{result.items.map((item) => (
<tr key={item.name}>
<td>
<Button theme="borderless" onClick={() => setSelected(item)}>
{item.local.favorite ? "已收藏 · " : ""}
{item.name}
</Button>
</td>
<td>{item.category}</td>
<td>
<code>{item.definition}</code>
</td>
</tr>
))}
</tbody>
</table>
{!result.items.length && (
<p>暂无算子。连接平台并同步目录,或调整筛选条件。</p>
)}
</ResearchList>
<ResearchDetail
title={selected?.name || "算子详情"}
visible={active && !!selected}
onClose={() => setSelected(null)}
>
{error && <Banner type="danger" description={error} />}
{selected && (
<section className="research-card">
<h3>{selected.name}</h3>
<p>{selected.description}</p>
<code>{selected.definition}</code>
{selected.example && <pre>{selected.example}</pre>}
{!!selected.parameters && (
<pre>{JSON.stringify(selected.parameters, null, 2)}</pre>
)}
<p>适用:{selected.scope?.join("、") || "平台未提供"}</p>
<label>
研究备注
<TextArea
aria-label="算子研究备注"
value={selected.local.note}
onChange={(note) =>
setSelected({
...selected,
local: { ...selected.local, note },
})
}
autosize={{ minRows: 4, maxRows: 8 }}
/>
</label>
<Checkbox
checked={selected.local.favorite}
onChange={(event) =>
setSelected({
...selected,
local: {
...selected.local,
favorite: !!event.target.checked,
},
})
}
>
收藏算子
</Checkbox>
<div className="research-toolbar">
<Button
disabled={busy}
onClick={() =>
void task(async () => {
const result = await patch<{ version: number }>(
`/catalog/operators/${encodeURIComponent(selected.name)}/research`,
selected.local,
);
setSelected({
...selected,
local: { ...selected.local, version: result.version },
});
await load();
})
}
>
保存本地备注
</Button>
</div>
</section>
)}
</ResearchDetail>
</div>
);
}