feat: 支持研究素材删除并统一 Semi 表格
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { Button, Popconfirm } from "@douyinfe/semi-ui-19";
|
||||
|
||||
/** Remove a research item from its list while keeping historical references. */
|
||||
export function DeleteResearchButton({
|
||||
name,
|
||||
label,
|
||||
disabled,
|
||||
onConfirm,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
onConfirm: () => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<Popconfirm
|
||||
title={`删除${label}“${name}”?`}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okType="danger"
|
||||
cancelButtonProps={{ autoFocus: true }}
|
||||
disabled={disabled}
|
||||
onConfirm={onConfirm}
|
||||
>
|
||||
<Button type="danger" theme="borderless" disabled={disabled}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { Banner, Button, Checkbox, Tag, Toast } from "@douyinfe/semi-ui-19";
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Checkbox,
|
||||
Table,
|
||||
Tag,
|
||||
Toast,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
import { post, formatTime } from "../api";
|
||||
import type { UIAction } from "../ai/types";
|
||||
import type { Experiment } from "./workspaceTypes";
|
||||
@@ -7,13 +14,18 @@ import { EvaluationPanel } from "./EvaluationPanel";
|
||||
import { LineagePanel } from "./LineagePanel";
|
||||
import { ResearchSelect } from "./ResearchSelect";
|
||||
import { validationLabel } from "./workspaceTypes";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
|
||||
export function ExperimentView({
|
||||
experiment,
|
||||
onAction,
|
||||
onDelete,
|
||||
deleting,
|
||||
}: {
|
||||
experiment: Experiment;
|
||||
onAction: (action: UIAction) => void;
|
||||
onDelete?: () => Promise<void>;
|
||||
deleting?: boolean;
|
||||
}) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [assessment, setAssessment] = useState(false);
|
||||
@@ -61,38 +73,59 @@ export function ExperimentView({
|
||||
</Button>
|
||||
</div>
|
||||
<p>{experiment.hypothesis}</p>
|
||||
{experiment.archived ? (
|
||||
<Banner
|
||||
type="info"
|
||||
description="此生成记录已删除,当前显示保留的候选与来源。"
|
||||
/>
|
||||
) : (
|
||||
onDelete && (
|
||||
<DeleteResearchButton
|
||||
name={experiment.name}
|
||||
label="生成记录"
|
||||
disabled={busy || deleting}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Banner
|
||||
type="info"
|
||||
description="候选已保存为不可变研究记录。本地校验不保证平台可执行;在回测预览中确认后才开始模拟。"
|
||||
/>
|
||||
<div className="research-table-scroll">
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>选择</th>
|
||||
<th>候选表达式</th>
|
||||
<th>市场与设置</th>
|
||||
<th>校验</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{experiment.candidates.map((candidate) => (
|
||||
<tr key={candidate.client_item_id}>
|
||||
<td>
|
||||
<Checkbox
|
||||
aria-label={`选择候选 ${candidate.client_item_id}`}
|
||||
disabled={candidate.validation.status !== "valid"}
|
||||
checked={selected.includes(candidate.client_item_id)}
|
||||
onChange={(event) =>
|
||||
setSelected((old) =>
|
||||
event.target.checked
|
||||
? [...old, candidate.client_item_id]
|
||||
: old.filter((id) => id !== candidate.client_item_id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Table<Experiment["candidates"][number]>
|
||||
className="research-table"
|
||||
dataSource={experiment.candidates}
|
||||
rowKey="client_item_id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
style={{ minWidth: 840 }}
|
||||
empty="暂无候选表达式。"
|
||||
columns={[
|
||||
{
|
||||
title: "选择",
|
||||
key: "selection",
|
||||
width: 64,
|
||||
render: (_, candidate) => (
|
||||
<Checkbox
|
||||
aria-label={`选择候选 ${candidate.client_item_id}`}
|
||||
disabled={candidate.validation.status !== "valid"}
|
||||
checked={selected.includes(candidate.client_item_id)}
|
||||
onChange={(event) =>
|
||||
setSelected((old) =>
|
||||
event.target.checked
|
||||
? [...old, candidate.client_item_id]
|
||||
: old.filter((id) => id !== candidate.client_item_id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "候选表达式",
|
||||
key: "expression",
|
||||
render: (_, candidate) => (
|
||||
<>
|
||||
<code>{candidate.expression}</code>
|
||||
<details>
|
||||
<summary>绑定与改动</summary>
|
||||
@@ -107,15 +140,29 @@ export function ExperimentView({
|
||||
)}
|
||||
</pre>
|
||||
</details>
|
||||
</td>
|
||||
<td>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "市场与设置",
|
||||
key: "settings",
|
||||
width: 240,
|
||||
render: (_, candidate) => (
|
||||
<>
|
||||
{candidate.settings.region} / {candidate.settings.universe} /
|
||||
D{candidate.settings.delay}
|
||||
<br />
|
||||
{candidate.settings.neutralization} · decay{" "}
|
||||
{candidate.settings.decay}
|
||||
</td>
|
||||
<td>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "校验",
|
||||
key: "validation",
|
||||
width: 180,
|
||||
render: (_, candidate) => (
|
||||
<>
|
||||
<Tag
|
||||
color={
|
||||
candidate.validation.status === "valid"
|
||||
@@ -139,11 +186,11 @@ export function ExperimentView({
|
||||
</p>
|
||||
)),
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="research-lineage">
|
||||
<strong>研究来源</strong>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Button,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TextArea,
|
||||
Toast,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
@@ -13,6 +14,7 @@ import { IconAIEditLevel3 } from "@douyinfe/semi-icons";
|
||||
import { api, post } from "../api";
|
||||
import type { PageContext, UIAction } from "../ai/types";
|
||||
import { TemplateEditor } from "./TemplateEditor";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
import { ResearchSelect } from "./ResearchSelect";
|
||||
import { blankTemplate } from "./workspaceTypes";
|
||||
import type { Asset, InputSnapshot, Template } from "./workspaceTypes";
|
||||
@@ -71,6 +73,9 @@ export function FeaturesPage({
|
||||
.then(([list, fixed]) => {
|
||||
setItems(list.items);
|
||||
setTotal(list.total);
|
||||
setPage((current) =>
|
||||
Math.min(current, Math.max(1, Math.ceil(list.total / 25))),
|
||||
);
|
||||
setInputs(fixed.items);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -131,6 +136,21 @@ export function FeaturesPage({
|
||||
setRevision((n) => n + 1);
|
||||
return saved;
|
||||
}
|
||||
async function remove(item: FeatureAsset) {
|
||||
await api(`/research/assets/${item.id}?version=${item.version}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
setItems((old) => old.filter((entry) => entry.id !== item.id));
|
||||
setTotal((old) => Math.max(0, old - 1));
|
||||
if (asset?.id === item.id) {
|
||||
setDetailOpen(false);
|
||||
setAsset(null);
|
||||
setDraft(blank());
|
||||
setVersions([]);
|
||||
}
|
||||
setRevision((n) => n + 1);
|
||||
Toast.success("特征方案已删除");
|
||||
}
|
||||
return (
|
||||
<section className="research-workspace research-list-page">
|
||||
{error && !detailOpen && <Banner type="danger" description={error} />}
|
||||
@@ -176,40 +196,67 @@ export function FeaturesPage({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>方案名称</th>
|
||||
<th>版本</th>
|
||||
<th>经济假设</th>
|
||||
<th>固定输入</th>
|
||||
<th>处理步骤</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() => void run("load", () => load(item))}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>v{item.version}</td>
|
||||
<td>{item.content.hypothesis || "—"}</td>
|
||||
<td>{item.content.input_ids.length}</td>
|
||||
<td>{item.content.steps.length}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!items.length && (
|
||||
<p className="research-list-empty">
|
||||
暂无特征方案,请新建方案或调整筛选条件。
|
||||
</p>
|
||||
)}
|
||||
<Table<FeatureAsset>
|
||||
className="research-table"
|
||||
dataSource={items}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 800 }}
|
||||
empty="暂无特征方案,请新建方案或调整筛选条件。"
|
||||
columns={[
|
||||
{
|
||||
title: "方案名称",
|
||||
key: "name",
|
||||
width: 200,
|
||||
render: (_, item) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() => void run("load", () => load(item))}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: 80,
|
||||
render: (_, item) => <>v{item.version}</>,
|
||||
},
|
||||
{
|
||||
title: "经济假设",
|
||||
key: "hypothesis",
|
||||
render: (_, item) => item.content.hypothesis || "—",
|
||||
},
|
||||
{
|
||||
title: "固定输入",
|
||||
key: "inputs",
|
||||
width: 100,
|
||||
render: (_, item) => item.content.input_ids.length,
|
||||
},
|
||||
{
|
||||
title: "处理步骤",
|
||||
key: "steps",
|
||||
width: 100,
|
||||
render: (_, item) => item.content.steps.length,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 80,
|
||||
render: (_, item) => (
|
||||
<DeleteResearchButton
|
||||
name={item.name}
|
||||
label="特征方案"
|
||||
disabled={!!busy}
|
||||
onConfirm={() => run("delete", () => remove(item))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResearchList>
|
||||
<ResearchDetail
|
||||
width="min(100vw, max(640px, 60vw))"
|
||||
@@ -218,6 +265,12 @@ export function FeaturesPage({
|
||||
onClose={() => setDetailOpen(false)}
|
||||
>
|
||||
{error && <Banner type="danger" description={error} />}
|
||||
{asset?.archived && (
|
||||
<Banner
|
||||
type="info"
|
||||
description="此特征方案已删除,当前显示保留的历史版本。"
|
||||
/>
|
||||
)}
|
||||
<div className="research-main">
|
||||
<div className="inline-actions">
|
||||
{asset && (
|
||||
@@ -401,7 +454,7 @@ export function FeaturesPage({
|
||||
<Button
|
||||
theme="solid"
|
||||
loading={busy === "save"}
|
||||
disabled={Boolean(busy)}
|
||||
disabled={Boolean(busy) || asset?.archived}
|
||||
onClick={() =>
|
||||
void run("save", async () => {
|
||||
await save();
|
||||
@@ -412,7 +465,7 @@ export function FeaturesPage({
|
||||
保存特征方案
|
||||
</Button>
|
||||
<Button
|
||||
disabled={Boolean(busy)}
|
||||
disabled={Boolean(busy) || asset?.archived}
|
||||
loading={busy === "convert"}
|
||||
onClick={() =>
|
||||
void run("convert", async () => {
|
||||
@@ -432,6 +485,14 @@ export function FeaturesPage({
|
||||
>
|
||||
保存并转为模板
|
||||
</Button>
|
||||
{asset && !asset.archived && (
|
||||
<DeleteResearchButton
|
||||
name={asset.name}
|
||||
label="特征方案"
|
||||
disabled={!!busy}
|
||||
onConfirm={() => run("delete", () => remove(asset))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="research-hint">
|
||||
保存步骤不会计算原始时间序列。转换后的模板保留此方案的完整版本,回测前仍需展开校验并确认候选。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Banner, Button, Tag, Toast } from "@douyinfe/semi-ui-19";
|
||||
import { Banner, Button, Table, Tag, Toast } from "@douyinfe/semi-ui-19";
|
||||
import { useState } from "react";
|
||||
import { post } from "../api";
|
||||
import type { UIAction } from "../ai/types";
|
||||
@@ -98,27 +98,43 @@ export function FlowRunView({
|
||||
</pre>
|
||||
</details>
|
||||
<div className="research-table-scroll">
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>轮次 / 阶段</th>
|
||||
<th>状态</th>
|
||||
<th>产物与候选池</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{run.steps.map((step) => (
|
||||
<tr key={step.id}>
|
||||
<td>
|
||||
<Table<FlowRun["steps"][number]>
|
||||
className="research-table"
|
||||
dataSource={run.steps}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
style={{ minWidth: 680 }}
|
||||
empty="暂无运行步骤。"
|
||||
columns={[
|
||||
{
|
||||
title: "轮次 / 阶段",
|
||||
key: "stage",
|
||||
width: 170,
|
||||
render: (_, step) => (
|
||||
<>
|
||||
{step.round} ·{" "}
|
||||
{run.definition.nodes.find((n) => n.id === step.node_id)
|
||||
?.label || step.node_id}
|
||||
</td>
|
||||
<td>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 140,
|
||||
render: (_, step) => (
|
||||
<>
|
||||
{flowStatus[step.status] || step.status}
|
||||
{step.error && <p>{step.error}</p>}
|
||||
</td>
|
||||
<td>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "产物与候选池",
|
||||
key: "output",
|
||||
render: (_, step) => (
|
||||
<>
|
||||
<div className="inline-actions">
|
||||
{step.output.template && (
|
||||
<Button
|
||||
@@ -184,11 +200,11 @@ export function FlowRunView({
|
||||
{JSON.stringify(step.output, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Table,
|
||||
TextArea,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
import { api, patch, post, formatTime } from "../api";
|
||||
@@ -138,34 +139,40 @@ export function OperatorsPage({ active }: { active: boolean }) {
|
||||
</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>
|
||||
)}
|
||||
<Table<Operator>
|
||||
className="research-table"
|
||||
dataSource={result.items}
|
||||
rowKey="name"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 700 }}
|
||||
empty="暂无算子。连接平台并同步目录,或调整筛选条件。"
|
||||
columns={[
|
||||
{
|
||||
title: "算子",
|
||||
key: "name",
|
||||
width: 230,
|
||||
render: (_, item) => (
|
||||
<Button theme="borderless" onClick={() => setSelected(item)}>
|
||||
{item.local.favorite ? "已收藏 · " : ""}
|
||||
{item.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
key: "category",
|
||||
width: 150,
|
||||
render: (_, item) => item.category,
|
||||
},
|
||||
{
|
||||
title: "定义",
|
||||
key: "definition",
|
||||
render: (_, item) => <code>{item.definition}</code>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResearchList>
|
||||
<ResearchDetail
|
||||
title={selected?.name || "算子详情"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ResearchList, ResearchDetail } from "./ResearchList";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Banner, Button, Pagination } from "@douyinfe/semi-ui-19";
|
||||
import { Banner, Button, Pagination, Table } from "@douyinfe/semi-ui-19";
|
||||
import { api, formatTime } from "../api";
|
||||
import type { PageContext, UIAction } from "../ai/types";
|
||||
import { FlowLaunchForm } from "./FlowLaunchForm";
|
||||
@@ -99,33 +99,45 @@ export function PipelinePage({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>研究名称</th>
|
||||
<th>状态</th>
|
||||
<th>轮次</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Button theme="borderless" onClick={() => select(item.id)}>
|
||||
{item.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>{flowStatus[item.status]}</td>
|
||||
<td>{item.round}</td>
|
||||
<td>{formatTime(item.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!items.length && (
|
||||
<p className="research-list-empty">暂无研究运行,请新建自动研究。</p>
|
||||
)}
|
||||
<Table<FlowRun>
|
||||
className="research-table"
|
||||
dataSource={items}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 640 }}
|
||||
empty="暂无研究运行,请新建自动研究。"
|
||||
columns={[
|
||||
{
|
||||
title: "研究名称",
|
||||
key: "name",
|
||||
render: (_, item) => (
|
||||
<Button theme="borderless" onClick={() => select(item.id)}>
|
||||
{item.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 140,
|
||||
render: (_, item) => flowStatus[item.status],
|
||||
},
|
||||
{
|
||||
title: "轮次",
|
||||
key: "round",
|
||||
width: 80,
|
||||
render: (_, item) => item.round,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "created_at",
|
||||
width: 190,
|
||||
render: (_, item) => formatTime(item.created_at),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResearchList>
|
||||
<ResearchDetail
|
||||
title={creating ? "新建自动研究" : "研究运行详情"}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Pagination,
|
||||
Table,
|
||||
TextArea,
|
||||
Toast,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
@@ -422,76 +423,95 @@ export function QuantFlowPage({
|
||||
>
|
||||
{mode === "runs" ? (
|
||||
<>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>运行名称</th>
|
||||
<th>状态</th>
|
||||
<th>轮次</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() => {
|
||||
setRunId(r.id);
|
||||
setRun(null);
|
||||
localStorage.setItem(
|
||||
"quantflow-selected-run",
|
||||
r.id,
|
||||
);
|
||||
navigate(`/runs/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{r.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>{flowStatus[r.status]}</td>
|
||||
<td>{r.round}</td>
|
||||
<td>{formatTime(r.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!runs.length && (
|
||||
<p className="research-list-empty">暂无流程运行记录。</p>
|
||||
)}
|
||||
<Table<FlowRun>
|
||||
className="research-table"
|
||||
dataSource={runs}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 640 }}
|
||||
empty="暂无流程运行记录。"
|
||||
columns={[
|
||||
{
|
||||
title: "运行名称",
|
||||
key: "name",
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() => {
|
||||
setRunId(r.id);
|
||||
setRun(null);
|
||||
localStorage.setItem("quantflow-selected-run", r.id);
|
||||
navigate(`/runs/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{r.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 140,
|
||||
render: (_, r) => flowStatus[r.status],
|
||||
},
|
||||
{
|
||||
title: "轮次",
|
||||
key: "round",
|
||||
width: 80,
|
||||
render: (_, r) => r.round,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "created_at",
|
||||
width: 190,
|
||||
render: (_, r) => formatTime(r.created_at),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>流程名称</th>
|
||||
<th>版本</th>
|
||||
<th>节点数量</th>
|
||||
<th>连接数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assets.map((a) => (
|
||||
<tr key={a.id}>
|
||||
<td>
|
||||
<Button theme="borderless" onClick={() => void open(a)}>
|
||||
{a.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>v{a.version}</td>
|
||||
<td>{a.content.nodes.length}</td>
|
||||
<td>{a.content.edges.length}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!assets.length && (
|
||||
<p className="research-list-empty">
|
||||
暂无流程,请新建流程或复制固定配方。
|
||||
</p>
|
||||
)}
|
||||
<Table<WorkflowAsset>
|
||||
className="research-table"
|
||||
dataSource={assets}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 640 }}
|
||||
empty="暂无流程,请新建流程或复制固定配方。"
|
||||
columns={[
|
||||
{
|
||||
title: "流程名称",
|
||||
key: "name",
|
||||
render: (_, a) => (
|
||||
<Button theme="borderless" onClick={() => void open(a)}>
|
||||
{a.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: 80,
|
||||
render: (_, a) => <>v{a.version}</>,
|
||||
},
|
||||
{
|
||||
title: "节点数量",
|
||||
key: "nodes",
|
||||
width: 110,
|
||||
render: (_, a) => a.content.nodes.length,
|
||||
},
|
||||
{
|
||||
title: "连接数量",
|
||||
key: "edges",
|
||||
width: 110,
|
||||
render: (_, a) => a.content.edges.length,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ResearchList>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
Table,
|
||||
TextArea,
|
||||
Toast,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from "./workspaceTypes";
|
||||
import { TemplateEditor } from "./TemplateEditor";
|
||||
import { ExperimentView } from "./ExperimentView";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
import { ComparisonPanel } from "./ComparisonPanel";
|
||||
import "./workspace.css";
|
||||
|
||||
@@ -107,6 +109,12 @@ export function ResearchWorkspace({
|
||||
if (sequence !== refreshSequence.current) return;
|
||||
setAssetTotal(nextAssets.total);
|
||||
setHistoryTotal(nextHistory.total);
|
||||
setAssetPage((current) =>
|
||||
Math.min(current, Math.max(0, Math.ceil(nextAssets.total / 25) - 1)),
|
||||
);
|
||||
setHistoryPage((current) =>
|
||||
Math.min(current, Math.max(0, Math.ceil(nextHistory.total / 25) - 1)),
|
||||
);
|
||||
setAssets(nextAssets.items);
|
||||
setInputs(nextInputs.items);
|
||||
setHistory(nextHistory.items);
|
||||
@@ -236,6 +244,31 @@ export function ResearchWorkspace({
|
||||
setTemplate(next.content);
|
||||
await refresh();
|
||||
}
|
||||
async function removeAsset(item: Asset) {
|
||||
await api(`/research/assets/${item.id}?version=${item.version}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
setAssets((old) => old.filter((entry) => entry.id !== item.id));
|
||||
setAssetTotal((old) => Math.max(0, old - 1));
|
||||
if (asset?.id === item.id) {
|
||||
if (detailMode === "editor") setDetailOpen(false);
|
||||
setAsset(null);
|
||||
setTemplate(blankTemplate());
|
||||
}
|
||||
Toast.success("模板已删除");
|
||||
await refresh();
|
||||
}
|
||||
async function removeExperiment(item: { id: string }) {
|
||||
await api(`/research/experiments/${item.id}`, { method: "DELETE" });
|
||||
setHistory((old) => old.filter((entry) => entry.id !== item.id));
|
||||
setHistoryTotal((old) => Math.max(0, old - 1));
|
||||
if (experiment?.id === item.id) {
|
||||
if (detailMode === "experiment") setDetailOpen(false);
|
||||
setExperiment(null);
|
||||
}
|
||||
Toast.success("生成记录已删除");
|
||||
await refresh();
|
||||
}
|
||||
async function expand() {
|
||||
if (dirty) throw new Error("请先保存模板版本,再生成可追溯候选");
|
||||
const next = await post<Experiment>("/research/experiments", {
|
||||
@@ -344,92 +377,137 @@ export function ResearchWorkspace({
|
||||
>
|
||||
{listMode === "assets" ? (
|
||||
<>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模板名称</th>
|
||||
<th>版本</th>
|
||||
<th>表达式</th>
|
||||
<th>分类</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assets.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
void task("读取模板", async () => {
|
||||
const next = await api<Asset>(
|
||||
`/research/assets/${item.id}`,
|
||||
);
|
||||
setAsset(next);
|
||||
setTemplate(next.content);
|
||||
setExperiment(null);
|
||||
setDetailMode("editor");
|
||||
setDetailOpen(true);
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>v{item.version}</td>
|
||||
<td>
|
||||
<code>{item.content.expression}</code>
|
||||
</td>
|
||||
<td>
|
||||
{item.content.category === "fragment" ? "片段" : "模板"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!assets.length && (
|
||||
<p className="research-list-empty">
|
||||
还没有模板。新建或导入一个模板开始研究。
|
||||
</p>
|
||||
)}
|
||||
<Table<Asset>
|
||||
className="research-table"
|
||||
dataSource={assets}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 720 }}
|
||||
empty="还没有模板。新建或导入一个模板开始研究。"
|
||||
columns={[
|
||||
{
|
||||
title: "模板名称",
|
||||
key: "name",
|
||||
render: (_, item) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
void task("读取模板", async () => {
|
||||
const next = await api<Asset>(
|
||||
`/research/assets/${item.id}`,
|
||||
);
|
||||
setAsset(next);
|
||||
setTemplate(next.content);
|
||||
setExperiment(null);
|
||||
setDetailMode("editor");
|
||||
setDetailOpen(true);
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: 80,
|
||||
render: (_, item) => <>v{item.version}</>,
|
||||
},
|
||||
{
|
||||
title: "表达式",
|
||||
key: "expression",
|
||||
width: 260,
|
||||
render: (_, item) => <code>{item.content.expression}</code>,
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
key: "category",
|
||||
width: 90,
|
||||
render: (_, item) =>
|
||||
item.content.category === "fragment" ? "片段" : "模板",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 80,
|
||||
render: (_, item) => (
|
||||
<DeleteResearchButton
|
||||
name={item.name}
|
||||
label="模板"
|
||||
disabled={!!busy}
|
||||
onConfirm={() =>
|
||||
task("删除模板", () => removeAsset(item))
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<table className="research-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>研究名称</th>
|
||||
<th>候选数量</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{history.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
void task("读取实验", async () => {
|
||||
setExperiment(
|
||||
await api(`/research/experiments/${item.id}`),
|
||||
);
|
||||
setDetailMode("experiment");
|
||||
setDetailOpen(true);
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
</td>
|
||||
<td>{item.total}</td>
|
||||
<td>{formatTime(item.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!history.length && (
|
||||
<p className="research-list-empty">暂无生成记录。</p>
|
||||
)}
|
||||
<Table<(typeof history)[number]>
|
||||
className="research-table"
|
||||
dataSource={history}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
sticky
|
||||
style={{ minWidth: 640 }}
|
||||
empty="暂无生成记录。"
|
||||
columns={[
|
||||
{
|
||||
title: "研究名称",
|
||||
key: "name",
|
||||
render: (_, item) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
void task("读取实验", async () => {
|
||||
setExperiment(
|
||||
await api(`/research/experiments/${item.id}`),
|
||||
);
|
||||
setDetailMode("experiment");
|
||||
setDetailOpen(true);
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "候选数量",
|
||||
key: "total",
|
||||
width: 110,
|
||||
render: (_, item) => item.total,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "created_at",
|
||||
width: 190,
|
||||
render: (_, item) => formatTime(item.created_at),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 80,
|
||||
render: (_, item) => (
|
||||
<DeleteResearchButton
|
||||
name={item.name}
|
||||
label="生成记录"
|
||||
disabled={!!busy}
|
||||
onConfirm={() =>
|
||||
task("删除生成记录", () => removeExperiment(item))
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ResearchList>
|
||||
@@ -452,10 +530,20 @@ export function ResearchWorkspace({
|
||||
key={experiment.id}
|
||||
experiment={experiment}
|
||||
onAction={onAction}
|
||||
deleting={!!busy}
|
||||
onDelete={() =>
|
||||
task("删除生成记录", () => removeExperiment(experiment))
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="research-main">
|
||||
{asset?.archived && (
|
||||
<Banner
|
||||
type="info"
|
||||
description="此模板已删除,当前显示保留的历史版本;可另存新模板继续编辑。"
|
||||
/>
|
||||
)}
|
||||
<section className="research-card">
|
||||
<h3>研究输入与假设</h3>
|
||||
{page === "variants" && (
|
||||
@@ -619,11 +707,21 @@ export function ResearchWorkspace({
|
||||
)}
|
||||
<div className="research-toolbar">
|
||||
<Button
|
||||
disabled={!!busy}
|
||||
disabled={!!busy || asset?.archived}
|
||||
onClick={() => void task("保存模板", save)}
|
||||
>
|
||||
保存模板版本
|
||||
</Button>
|
||||
{asset && !asset.archived && (
|
||||
<DeleteResearchButton
|
||||
name={asset.name}
|
||||
label="模板"
|
||||
disabled={!!busy}
|
||||
onConfirm={() =>
|
||||
task("删除模板", () => removeAsset(asset))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span>组合规模:{combination}</span>
|
||||
</div>
|
||||
<div className="research-form-grid">
|
||||
@@ -776,6 +874,10 @@ export function ResearchWorkspace({
|
||||
key={experiment.id}
|
||||
experiment={experiment}
|
||||
onAction={onAction}
|
||||
deleting={!!busy}
|
||||
onDelete={() =>
|
||||
task("删除生成记录", () => removeExperiment(experiment))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{page === "templates" && (
|
||||
|
||||
@@ -104,25 +104,8 @@
|
||||
.research-table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.research-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
}
|
||||
.research-table th {
|
||||
color: var(--semi-color-text-2);
|
||||
font-weight: 500;
|
||||
background: var(--semi-color-fill-0);
|
||||
}
|
||||
.research-table th,
|
||||
.research-table td {
|
||||
text-align: left;
|
||||
.research-table .semi-table-row-cell {
|
||||
vertical-align: top;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
}
|
||||
.research-table td {
|
||||
max-width: 350px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.research-table details {
|
||||
@@ -356,16 +339,6 @@
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.research-list-scroll .research-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: var(--canvas);
|
||||
}
|
||||
.research-list-empty {
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.research-detail .semi-sidesheet-body {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
|
||||
@@ -24,6 +24,8 @@ export type Asset = {
|
||||
name: string;
|
||||
kind: string;
|
||||
version: number;
|
||||
latest_version?: number;
|
||||
archived?: boolean;
|
||||
content: Template;
|
||||
provenance?: {
|
||||
feature?: {
|
||||
@@ -59,6 +61,7 @@ export type ResearchCandidate = Candidate & {
|
||||
};
|
||||
export type Experiment = {
|
||||
id: string;
|
||||
archived?: boolean;
|
||||
name: string;
|
||||
hypothesis: string;
|
||||
kind: string;
|
||||
|
||||
Reference in New Issue
Block a user