232 lines
6.8 KiB
TypeScript
232 lines
6.8 KiB
TypeScript
|
|
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">
|
||
|
|
<header className="research-page-heading">
|
||
|
|
<div>
|
||
|
|
<h2>算子库</h2>
|
||
|
|
<p>
|
||
|
|
平台定义只读,本地收藏和研究备注独立保存。同步时间:
|
||
|
|
{formatTime(result.fetched_at)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
theme="solid"
|
||
|
|
loading={busy}
|
||
|
|
onClick={() =>
|
||
|
|
void task(async () => {
|
||
|
|
await post("/catalog/operators/refresh");
|
||
|
|
await load();
|
||
|
|
})
|
||
|
|
}
|
||
|
|
>
|
||
|
|
同步算子目录
|
||
|
|
</Button>
|
||
|
|
</header>
|
||
|
|
{error && <Banner type="danger" description={error} />}
|
||
|
|
<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>
|
||
|
|
<div className="research-operator-layout">
|
||
|
|
<section className="research-card">
|
||
|
|
<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>
|
||
|
|
)}
|
||
|
|
<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>
|
||
|
|
</section>
|
||
|
|
{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>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|