107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { Banner, Button, Empty, Toast } from "@douyinfe/semi-ui-19";
|
||
|
|
import { api } from "../api";
|
||
|
|
import type { UIAction } from "../ai/types";
|
||
|
|
import type { Experiment } from "./workspaceTypes";
|
||
|
|
type Graph = {
|
||
|
|
items: Experiment[];
|
||
|
|
edges: { child_id: string; parent_kind: string; parent_id: string }[];
|
||
|
|
truncated: boolean;
|
||
|
|
unresolved_experiment_ids: string[];
|
||
|
|
};
|
||
|
|
export function LineagePanel({
|
||
|
|
alphaId,
|
||
|
|
experimentId,
|
||
|
|
onAction,
|
||
|
|
}: {
|
||
|
|
alphaId?: string;
|
||
|
|
experimentId?: string;
|
||
|
|
onAction: (a: UIAction) => void;
|
||
|
|
}) {
|
||
|
|
const [graph, setGraph] = useState<Graph | null>(null);
|
||
|
|
const [focus, setFocus] = useState(experimentId);
|
||
|
|
useEffect(() => {
|
||
|
|
const c = new AbortController();
|
||
|
|
api<Graph>(
|
||
|
|
`/research/lineage?${focus ? `experiment_id=${encodeURIComponent(focus)}` : `alpha_id=${encodeURIComponent(alphaId || "")}`}`,
|
||
|
|
{ signal: c.signal },
|
||
|
|
)
|
||
|
|
.then(setGraph)
|
||
|
|
.catch((e) => {
|
||
|
|
if (!c.signal.aborted) Toast.error(e.message);
|
||
|
|
});
|
||
|
|
return () => c.abort();
|
||
|
|
}, [alphaId, focus]);
|
||
|
|
return (
|
||
|
|
<div className="detail-section">
|
||
|
|
<h3>变体关系</h3>
|
||
|
|
<p className="muted">
|
||
|
|
每个实验保留所有父来源;每次回测来源另见「研究来源」。选择实验可继续查看输入、设置差异、候选及回测。
|
||
|
|
</p>
|
||
|
|
{graph?.truncated && (
|
||
|
|
<Banner
|
||
|
|
type="info"
|
||
|
|
description="关系图按 100 个实验、8 层展示;可从边界实验继续展开。"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
{graph?.items.length === 0 && <Empty description="尚无本地变体关系" />}
|
||
|
|
{graph?.items.map((e) => (
|
||
|
|
<div className="research-card" key={e.id}>
|
||
|
|
<h4>{e.name}</h4>
|
||
|
|
<p>{e.hypothesis}</p>
|
||
|
|
<p>
|
||
|
|
方法:{e.evidence.method || e.kind} · 模板{" "}
|
||
|
|
{e.evidence.template?.name || "—"}{" "}
|
||
|
|
{e.evidence.template?.version
|
||
|
|
? `v${e.evidence.template.version}`
|
||
|
|
: ""}
|
||
|
|
</p>
|
||
|
|
<div className="inline-actions">
|
||
|
|
{e.parents.map((p) => (
|
||
|
|
<Button
|
||
|
|
key={`${p.kind}:${p.id}`}
|
||
|
|
size="small"
|
||
|
|
onClick={() =>
|
||
|
|
p.kind === "alpha"
|
||
|
|
? onAction({
|
||
|
|
type: "open_alpha",
|
||
|
|
alpha_id: p.id,
|
||
|
|
nonce: Date.now(),
|
||
|
|
})
|
||
|
|
: setFocus(p.id)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
父{p.kind === "alpha" ? " Alpha" : "实验"} {p.id.slice(0, 12)}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<p className="muted">
|
||
|
|
{e.candidates.length} 个候选 · {e.backtest_run_ids.length} 次回测
|
||
|
|
</p>
|
||
|
|
<div className="inline-actions">
|
||
|
|
<Button
|
||
|
|
onClick={() =>
|
||
|
|
onAction({
|
||
|
|
type: "open_experiment",
|
||
|
|
experiment_id: e.id,
|
||
|
|
nonce: Date.now(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
>
|
||
|
|
查看候选与来源
|
||
|
|
</Button>
|
||
|
|
<Button theme="borderless" onClick={() => setFocus(e.id)}>
|
||
|
|
以此实验展开关系
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{graph?.unresolved_experiment_ids.map((id) => (
|
||
|
|
<Button key={id} onClick={() => setFocus(id)}>
|
||
|
|
继续展开 {id}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|