2026-09-12 12:11:53 +08:00
|
|
|
|
import { SimulationSettingsEditor } from "../settings/SimulationSettingsEditor";
|
|
|
|
|
|
import { useSettingsOptions } from "../settings/useSettingsOptions";
|
2026-09-12 01:24:02 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ResearchDataInput,
|
|
|
|
|
|
researchSelection,
|
|
|
|
|
|
} from "../preparations/ResearchDataInput";
|
2026-09-09 09:29:30 +08:00
|
|
|
|
import { ResearchList, ResearchDetail } from "./ResearchList";
|
2026-09-08 21:29:21 +08:00
|
|
|
|
import { ResearchSelect } from "./ResearchSelect";
|
|
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Banner,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
InputNumber,
|
2026-09-11 23:21:08 +08:00
|
|
|
|
Table,
|
2026-09-08 21:29:21 +08:00
|
|
|
|
TextArea,
|
|
|
|
|
|
Toast,
|
2026-09-13 14:31:03 +08:00
|
|
|
|
Typography,
|
2026-09-08 21:29:21 +08:00
|
|
|
|
} from "@douyinfe/semi-ui-19";
|
2026-09-13 14:31:03 +08:00
|
|
|
|
import { WorkspaceTable } from "../components/WorkspaceTable";
|
2026-09-10 12:22:26 +08:00
|
|
|
|
import { ResearchInput } from "../ai/ResearchInput";
|
|
|
|
|
|
import { IconAIEditLevel3 } from "@douyinfe/semi-icons";
|
2026-09-08 21:29:21 +08:00
|
|
|
|
import { api, post, formatTime } from "../api";
|
|
|
|
|
|
import type { PageContext, UIAction } from "../ai/types";
|
|
|
|
|
|
import { initialSettings, type SimulationSettings } from "../backtests/types";
|
|
|
|
|
|
import {
|
|
|
|
|
|
blankTemplate,
|
|
|
|
|
|
type Asset,
|
|
|
|
|
|
type Experiment,
|
|
|
|
|
|
type InputSnapshot,
|
|
|
|
|
|
type Template,
|
|
|
|
|
|
} from "./workspaceTypes";
|
|
|
|
|
|
import { TemplateEditor } from "./TemplateEditor";
|
|
|
|
|
|
import { ExperimentView } from "./ExperimentView";
|
2026-09-11 23:21:08 +08:00
|
|
|
|
import { DeleteResearchButton } from "./DeleteResearchButton";
|
2026-09-08 21:29:21 +08:00
|
|
|
|
import { ComparisonPanel } from "./ComparisonPanel";
|
|
|
|
|
|
import "./workspace.css";
|
|
|
|
|
|
|
|
|
|
|
|
type ImportResult = {
|
|
|
|
|
|
templates: Template[];
|
|
|
|
|
|
digest: string;
|
|
|
|
|
|
conflicts: unknown[];
|
|
|
|
|
|
errors: unknown[];
|
|
|
|
|
|
differences?: unknown[];
|
|
|
|
|
|
};
|
|
|
|
|
|
export function ResearchWorkspace({
|
|
|
|
|
|
page,
|
|
|
|
|
|
active,
|
|
|
|
|
|
action,
|
|
|
|
|
|
onAction,
|
|
|
|
|
|
onContext,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
page: "templates" | "variants";
|
|
|
|
|
|
active: boolean;
|
|
|
|
|
|
action: UIAction | null;
|
|
|
|
|
|
onAction: (action: UIAction) => void;
|
|
|
|
|
|
onContext: (context: PageContext) => void;
|
|
|
|
|
|
}) {
|
2026-09-09 09:29:30 +08:00
|
|
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
|
|
|
|
const [detailMode, setDetailMode] = useState<"editor" | "experiment">(
|
|
|
|
|
|
"editor",
|
|
|
|
|
|
);
|
|
|
|
|
|
const [listMode, setListMode] = useState("assets");
|
2026-09-08 21:29:21 +08:00
|
|
|
|
const processedAction = useRef<number | null>(null);
|
|
|
|
|
|
const [assets, setAssets] = useState<Asset[]>([]);
|
|
|
|
|
|
const [inputs, setInputs] = useState<InputSnapshot[]>([]);
|
|
|
|
|
|
const [inputIds, setInputIds] = useState<string[]>([]);
|
|
|
|
|
|
const [template, setTemplate] = useState<Template>(blankTemplate);
|
|
|
|
|
|
const [asset, setAsset] = useState<Asset | null>(null);
|
|
|
|
|
|
const [hypothesis, setHypothesis] = useState("");
|
|
|
|
|
|
const [parent, setParent] = useState("");
|
|
|
|
|
|
const [method, setMethod] = useState("structure");
|
|
|
|
|
|
const [settings, setSettings] = useState<SimulationSettings>(initialSettings);
|
2026-09-12 12:11:53 +08:00
|
|
|
|
const options = useSettingsOptions(active && detailOpen);
|
|
|
|
|
|
const [settingsValid, setSettingsValid] = useState(false);
|
2026-09-08 21:29:21 +08:00
|
|
|
|
const [mode, setMode] = useState("all");
|
|
|
|
|
|
const [limit, setLimit] = useState(100);
|
|
|
|
|
|
const [seed, setSeed] = useState(0);
|
|
|
|
|
|
const [experiment, setExperiment] = useState<Experiment | null>(null);
|
|
|
|
|
|
const [history, setHistory] = useState<
|
|
|
|
|
|
{ id: string; name: string; created_at: string; total: number }[]
|
|
|
|
|
|
>([]);
|
|
|
|
|
|
const [busy, setBusy] = useState("");
|
|
|
|
|
|
const [error, setError] = useState("");
|
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
|
const [assetPage, setAssetPage] = useState(0);
|
|
|
|
|
|
const [historyPage, setHistoryPage] = useState(0);
|
|
|
|
|
|
const [assetTotal, setAssetTotal] = useState(0);
|
|
|
|
|
|
const [historyTotal, setHistoryTotal] = useState(0);
|
|
|
|
|
|
const refreshSequence = useRef(0);
|
|
|
|
|
|
const [importText, setImportText] = useState("");
|
|
|
|
|
|
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
|
|
|
|
|
const selectedInputs = inputs.filter((input) => inputIds.includes(input.id));
|
|
|
|
|
|
const dirty =
|
|
|
|
|
|
!asset || JSON.stringify(template) !== JSON.stringify(asset.content);
|
|
|
|
|
|
async function task(label: string, action: () => Promise<void>) {
|
|
|
|
|
|
setBusy(label);
|
|
|
|
|
|
setError("");
|
|
|
|
|
|
try {
|
|
|
|
|
|
await action();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setError((error as Error).message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBusy("");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function refresh() {
|
|
|
|
|
|
const sequence = ++refreshSequence.current;
|
|
|
|
|
|
const [nextAssets, nextInputs, nextHistory] = await Promise.all([
|
|
|
|
|
|
api<{ items: Asset[]; total: number }>(
|
|
|
|
|
|
`/research/assets?kind=template&limit=25&offset=${assetPage * 25}&q=${encodeURIComponent(search)}`,
|
|
|
|
|
|
),
|
2026-09-12 01:24:02 +08:00
|
|
|
|
Promise.resolve({ items: inputs }),
|
2026-09-08 21:29:21 +08:00
|
|
|
|
api<{ items: typeof history; total: number }>(
|
|
|
|
|
|
`/research/experiments?limit=25&offset=${historyPage * 25}`,
|
|
|
|
|
|
),
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (sequence !== refreshSequence.current) return;
|
|
|
|
|
|
setAssetTotal(nextAssets.total);
|
|
|
|
|
|
setHistoryTotal(nextHistory.total);
|
2026-09-11 23:21:08 +08:00
|
|
|
|
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)),
|
|
|
|
|
|
);
|
2026-09-08 21:29:21 +08:00
|
|
|
|
setAssets(nextAssets.items);
|
|
|
|
|
|
setInputs(nextInputs.items);
|
|
|
|
|
|
setHistory(nextHistory.items);
|
|
|
|
|
|
}
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (active) void task("读取研究素材", refresh);
|
|
|
|
|
|
}, [active, search, assetPage, historyPage]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (active)
|
|
|
|
|
|
onContext({
|
|
|
|
|
|
page,
|
|
|
|
|
|
research_asset_id: asset?.id,
|
|
|
|
|
|
research_experiment_id: experiment?.id,
|
|
|
|
|
|
alpha_id: parent.split(/[,,\s]+/)[0] || undefined,
|
2026-09-12 01:24:02 +08:00
|
|
|
|
input_snapshot_id:
|
|
|
|
|
|
inputIds.length === 1 &&
|
|
|
|
|
|
!inputs.find((i) => i.id === inputIds[0])?.preparation_ref
|
|
|
|
|
|
? inputIds[0]
|
|
|
|
|
|
: undefined,
|
2026-09-08 21:29:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
}, [active, page, asset?.id, experiment?.id, parent, inputIds, onContext]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!active || !action || processedAction.current === action.nonce) return;
|
|
|
|
|
|
processedAction.current = action.nonce;
|
2026-09-09 09:29:30 +08:00
|
|
|
|
if (
|
|
|
|
|
|
["open_experiment", "open_template", "open_variant"].includes(action.type)
|
|
|
|
|
|
) {
|
|
|
|
|
|
setDetailOpen(true);
|
|
|
|
|
|
setDetailMode(
|
|
|
|
|
|
action.type === "open_experiment" ? "experiment" : "editor",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
if (action.type === "open_experiment")
|
|
|
|
|
|
void task("读取实验", async () =>
|
|
|
|
|
|
setExperiment(
|
|
|
|
|
|
await api(`/research/experiments/${action.experiment_id}`),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (action.type === "open_template")
|
|
|
|
|
|
void task("读取模板", async () => {
|
|
|
|
|
|
const next = await api<Asset>(
|
|
|
|
|
|
`/research/assets/${action.asset_id}${action.version ? `?version=${action.version}` : ""}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
setAsset(next);
|
|
|
|
|
|
setTemplate(next.content);
|
2026-09-08 21:52:46 +08:00
|
|
|
|
const feature = next.provenance?.feature;
|
|
|
|
|
|
if (feature) {
|
|
|
|
|
|
setInputIds(feature.content.input_ids);
|
|
|
|
|
|
setHypothesis(feature.content.hypothesis);
|
|
|
|
|
|
const fixed = feature.provenance?.inputs || [];
|
|
|
|
|
|
setInputs((old) => [
|
|
|
|
|
|
...old,
|
|
|
|
|
|
...fixed.filter((i) => !old.some((o) => o.id === i.id)),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const first = fixed[0];
|
|
|
|
|
|
if (first)
|
|
|
|
|
|
setSettings((old) => ({
|
|
|
|
|
|
...old,
|
|
|
|
|
|
region: first.scope.region,
|
|
|
|
|
|
universe: first.scope.universe,
|
|
|
|
|
|
delay: first.scope.delay,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
if (action.type === "open_variant") {
|
|
|
|
|
|
setParent(action.alpha_id);
|
|
|
|
|
|
setMethod("structure");
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [active, action]);
|
|
|
|
|
|
async function save() {
|
|
|
|
|
|
const next = asset
|
|
|
|
|
|
? await api<Asset>(`/research/assets/${asset.id}`, {
|
|
|
|
|
|
method: "PUT",
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
kind: "template",
|
|
|
|
|
|
version: asset.version,
|
|
|
|
|
|
content: template,
|
|
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
: await post<Asset>("/research/assets", {
|
|
|
|
|
|
kind: "template",
|
|
|
|
|
|
content: template,
|
|
|
|
|
|
});
|
|
|
|
|
|
setAsset(next);
|
|
|
|
|
|
setTemplate(next.content);
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
Toast.success(`已保存模板 v${next.version}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
async function generate() {
|
|
|
|
|
|
const next = await post<Asset>("/research/generate", {
|
|
|
|
|
|
name: template.name,
|
|
|
|
|
|
hypothesis,
|
2026-09-12 01:24:02 +08:00
|
|
|
|
...researchSelection(inputIds, inputs),
|
2026-09-08 21:29:21 +08:00
|
|
|
|
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
|
|
|
|
|
|
method: page === "variants" ? "structure" : "template",
|
|
|
|
|
|
});
|
|
|
|
|
|
setAsset(next);
|
|
|
|
|
|
setTemplate(next.content);
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
}
|
2026-09-11 23:21:08 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
async function expand() {
|
|
|
|
|
|
if (dirty) throw new Error("请先保存模板版本,再生成可追溯候选");
|
|
|
|
|
|
const next = await post<Experiment>("/research/experiments", {
|
|
|
|
|
|
asset_id: asset!.id,
|
|
|
|
|
|
version: asset!.version,
|
2026-09-12 01:24:02 +08:00
|
|
|
|
...researchSelection(inputIds, inputs),
|
2026-09-08 21:29:21 +08:00
|
|
|
|
hypothesis,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
limit,
|
|
|
|
|
|
seed,
|
|
|
|
|
|
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
|
|
|
|
|
|
});
|
|
|
|
|
|
setExperiment(next);
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
const combination = Object.values(template.variables)
|
|
|
|
|
|
.reduce(
|
|
|
|
|
|
(size, variable) =>
|
|
|
|
|
|
size * BigInt(new Set(variable.values.map(String)).size),
|
|
|
|
|
|
1n,
|
|
|
|
|
|
)
|
|
|
|
|
|
.toString();
|
|
|
|
|
|
return (
|
2026-09-09 09:29:30 +08:00
|
|
|
|
<div className="research-workspace research-list-page">
|
|
|
|
|
|
{error && !detailOpen && <Banner type="danger" description={error} />}
|
|
|
|
|
|
<ResearchList
|
|
|
|
|
|
resetKey={JSON.stringify([listMode, search, assetPage, historyPage])}
|
|
|
|
|
|
label={page === "templates" ? "模板工坊" : "Alpha 变体"}
|
|
|
|
|
|
filters={
|
|
|
|
|
|
<>
|
|
|
|
|
|
<ResearchSelect
|
|
|
|
|
|
label="研究列表"
|
|
|
|
|
|
value={listMode}
|
|
|
|
|
|
optionList={[
|
|
|
|
|
|
{ value: "assets", label: "模板库" },
|
|
|
|
|
|
{ value: "history", label: "生成记录" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(value) => setListMode(String(value))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{listMode === "assets" && (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
aria-label="搜索模板"
|
|
|
|
|
|
placeholder="搜索名称"
|
|
|
|
|
|
value={search}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
setSearch(value);
|
|
|
|
|
|
setAssetPage(0);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
actions={
|
|
|
|
|
|
<>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
<Button
|
2026-09-09 09:29:30 +08:00
|
|
|
|
theme="solid"
|
2026-09-08 21:29:21 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setAsset(null);
|
|
|
|
|
|
setTemplate(blankTemplate());
|
2026-09-09 09:29:30 +08:00
|
|
|
|
setExperiment(null);
|
|
|
|
|
|
setDetailMode("editor");
|
|
|
|
|
|
setDetailOpen(true);
|
2026-09-08 21:29:21 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
新建
|
|
|
|
|
|
</Button>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
<Button loading={!!busy} onClick={() => void task("刷新", refresh)}>
|
|
|
|
|
|
刷新素材
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</Button>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
pagination={
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
共 {listMode === "assets" ? assetTotal : historyTotal} 条
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="inline-actions">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
disabled={!(listMode === "assets" ? assetPage : historyPage)}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
listMode === "assets"
|
|
|
|
|
|
? setAssetPage(assetPage - 1)
|
|
|
|
|
|
: setHistoryPage(historyPage - 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
上一页
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
listMode === "assets"
|
|
|
|
|
|
? (assetPage + 1) * 25 >= assetTotal
|
|
|
|
|
|
: (historyPage + 1) * 25 >= historyTotal
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
listMode === "assets"
|
|
|
|
|
|
? setAssetPage(assetPage + 1)
|
|
|
|
|
|
: setHistoryPage(historyPage + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
下一页
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{listMode === "assets" ? (
|
2026-09-13 14:31:03 +08:00
|
|
|
|
<div className="research-template-table-container">
|
|
|
|
|
|
<WorkspaceTable<Asset>
|
|
|
|
|
|
key={JSON.stringify([search, assetPage])}
|
|
|
|
|
|
fill
|
2026-09-11 23:21:08 +08:00
|
|
|
|
dataSource={assets}
|
|
|
|
|
|
rowKey="id"
|
2026-09-13 14:31:03 +08:00
|
|
|
|
scroll={{ x: 800, y: "100%" }}
|
2026-09-11 23:21:08 +08:00
|
|
|
|
empty="还没有模板。新建或导入一个模板开始研究。"
|
|
|
|
|
|
columns={[
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "模板名称",
|
|
|
|
|
|
key: "name",
|
2026-09-13 14:31:03 +08:00
|
|
|
|
width: 280,
|
|
|
|
|
|
ellipsis: { showTitle: false },
|
2026-09-11 23:21:08 +08:00
|
|
|
|
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);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-09-13 14:31:03 +08:00
|
|
|
|
<Typography.Text
|
|
|
|
|
|
size="inherit"
|
|
|
|
|
|
style={{ width: "100%", color: "inherit" }}
|
|
|
|
|
|
ellipsis={{ showTooltip: true }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.name}
|
|
|
|
|
|
</Typography.Text>
|
2026-09-11 23:21:08 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "版本",
|
|
|
|
|
|
key: "version",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
render: (_, item) => <>v{item.version}</>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "表达式",
|
|
|
|
|
|
key: "expression",
|
2026-09-13 14:31:03 +08:00
|
|
|
|
width: undefined,
|
|
|
|
|
|
ellipsis: { showTitle: false },
|
|
|
|
|
|
render: (_, item) => (
|
|
|
|
|
|
<Typography.Text
|
|
|
|
|
|
className="research-template-expression"
|
|
|
|
|
|
style={{ width: "100%" }}
|
|
|
|
|
|
ellipsis={{ showTooltip: true }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.content.expression}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
),
|
2026-09-11 23:21:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "分类",
|
|
|
|
|
|
key: "category",
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
render: (_, item) =>
|
|
|
|
|
|
item.content.category === "fragment" ? "片段" : "模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
key: "actions",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
render: (_, item) => (
|
2026-09-13 14:31:03 +08:00
|
|
|
|
<div className="workspace-cell-actions">
|
|
|
|
|
|
<DeleteResearchButton
|
|
|
|
|
|
name={item.name}
|
|
|
|
|
|
label="模板"
|
|
|
|
|
|
disabled={!!busy}
|
|
|
|
|
|
onConfirm={() =>
|
|
|
|
|
|
task("删除模板", () => removeAsset(item))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-09-11 23:21:08 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
2026-09-13 14:31:03 +08:00
|
|
|
|
</div>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
2026-09-11 23:21:08 +08:00
|
|
|
|
<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))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ResearchList>
|
|
|
|
|
|
<ResearchDetail
|
2026-09-10 12:22:26 +08:00
|
|
|
|
width="min(100vw, max(640px, 60vw))"
|
2026-09-09 09:29:30 +08:00
|
|
|
|
title={
|
|
|
|
|
|
detailMode === "experiment"
|
|
|
|
|
|
? experiment?.name || "生成记录"
|
|
|
|
|
|
: page === "variants"
|
|
|
|
|
|
? "Alpha 变体详情"
|
|
|
|
|
|
: asset?.name || "新建模板"
|
|
|
|
|
|
}
|
|
|
|
|
|
visible={active && detailOpen}
|
|
|
|
|
|
onClose={() => setDetailOpen(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{error && <Banner type="danger" description={error} />}
|
|
|
|
|
|
{detailMode === "experiment" ? (
|
|
|
|
|
|
experiment && (
|
|
|
|
|
|
<ExperimentView
|
|
|
|
|
|
key={experiment.id}
|
|
|
|
|
|
experiment={experiment}
|
|
|
|
|
|
onAction={onAction}
|
2026-09-11 23:21:08 +08:00
|
|
|
|
deleting={!!busy}
|
|
|
|
|
|
onDelete={() =>
|
|
|
|
|
|
task("删除生成记录", () => removeExperiment(experiment))
|
|
|
|
|
|
}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="research-main">
|
2026-09-11 23:21:08 +08:00
|
|
|
|
{asset?.archived && (
|
|
|
|
|
|
<Banner
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
description="此模板已删除,当前显示保留的历史版本;可另存新模板继续编辑。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
<section className="research-card">
|
|
|
|
|
|
<h3>研究输入与假设</h3>
|
|
|
|
|
|
{page === "variants" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="research-methods">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
theme={method === "structure" ? "solid" : "light"}
|
|
|
|
|
|
onClick={() => setMethod("structure")}
|
|
|
|
|
|
>
|
|
|
|
|
|
结构与字段变体
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
theme={method === "settings" ? "solid" : "light"}
|
|
|
|
|
|
onClick={() => setMethod("settings")}
|
|
|
|
|
|
>
|
|
|
|
|
|
市场与设置变体
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{method === "structure"
|
|
|
|
|
|
? "神笔马良:保留种子与改动依据,研究新的表达式。"
|
|
|
|
|
|
: "缘分一道桥:保持表达式,为各目标市场重新固定输入后检查可用范围。"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
种子 Alpha
|
|
|
|
|
|
<Input
|
|
|
|
|
|
aria-label="种子 Alpha"
|
|
|
|
|
|
value={parent}
|
|
|
|
|
|
onChange={setParent}
|
|
|
|
|
|
placeholder="已同步的 Alpha ID"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<label>
|
|
|
|
|
|
固定研究输入
|
2026-09-12 01:24:02 +08:00
|
|
|
|
<ResearchDataInput
|
|
|
|
|
|
label="选择数据准备"
|
|
|
|
|
|
ids={inputIds}
|
|
|
|
|
|
inputs={inputs}
|
|
|
|
|
|
onChange={(ids, rows) => {
|
|
|
|
|
|
setInputs(rows);
|
|
|
|
|
|
setInputIds(ids);
|
|
|
|
|
|
const first = rows.find((r) => r.id === ids[0]);
|
|
|
|
|
|
if (first)
|
|
|
|
|
|
setSettings((old) => ({
|
|
|
|
|
|
...old,
|
|
|
|
|
|
region: first.scope.region,
|
|
|
|
|
|
universe: first.scope.universe,
|
|
|
|
|
|
delay: first.scope.delay,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p className="research-hint">
|
2026-09-12 01:24:02 +08:00
|
|
|
|
从数据准备选择集合;每个集合可包含同范围下多个数据集的字段。
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</p>
|
2026-09-10 12:22:26 +08:00
|
|
|
|
{page === "variants" && method === "settings" ? (
|
|
|
|
|
|
<label>
|
|
|
|
|
|
本次研究假设
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
aria-label="本次研究假设"
|
|
|
|
|
|
value={hypothesis}
|
|
|
|
|
|
onChange={setHypothesis}
|
|
|
|
|
|
autosize={{ minRows: 2, maxRows: 5 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="research-ai-section">
|
|
|
|
|
|
<h4>
|
|
|
|
|
|
<IconAIEditLevel3 aria-hidden="true" />
|
|
|
|
|
|
本次研究假设
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<ResearchInput
|
|
|
|
|
|
maxLength={10000}
|
|
|
|
|
|
label="本次研究假设"
|
|
|
|
|
|
value={hypothesis}
|
|
|
|
|
|
onChange={setHypothesis}
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
page === "variants"
|
|
|
|
|
|
? "描述希望保留的信号与要探索的变体方向…"
|
|
|
|
|
|
: "描述经济假设、信号方向与希望验证的关系…"
|
|
|
|
|
|
}
|
|
|
|
|
|
references={[
|
|
|
|
|
|
...selectedInputs.map((input) => ({
|
|
|
|
|
|
id: input.id,
|
2026-09-12 01:24:02 +08:00
|
|
|
|
name: `${input.name} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id}`,
|
2026-09-10 12:22:26 +08:00
|
|
|
|
})),
|
|
|
|
|
|
...(page === "variants" && parent
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "parent-alpha",
|
|
|
|
|
|
name: `种子 Alpha:${parent}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
: []),
|
|
|
|
|
|
]}
|
|
|
|
|
|
disabled={!!busy}
|
|
|
|
|
|
canSubmit={!!inputIds.length}
|
|
|
|
|
|
busy={busy === "AI 生成"}
|
|
|
|
|
|
submitLabel="AI 生成模板"
|
|
|
|
|
|
sendHotKey="shift+enter"
|
|
|
|
|
|
onSubmit={() => void task("AI 生成", generate)}
|
|
|
|
|
|
prompts={
|
|
|
|
|
|
page === "variants"
|
|
|
|
|
|
? ["保留种子的经济含义,探索不同时间窗口和字段组合"]
|
|
|
|
|
|
: ["研究短期价格反转与成交量之间的关系"]
|
|
|
|
|
|
}
|
|
|
|
|
|
hint={
|
|
|
|
|
|
inputIds.length
|
|
|
|
|
|
? "基于已关联的固定输入生成,保留假设供下一轮调整。"
|
|
|
|
|
|
: "请先关联固定研究输入。"
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
{page !== "variants" || method === "structure" ? (
|
|
|
|
|
|
<section className="research-card">
|
|
|
|
|
|
<div className="research-section-heading">
|
|
|
|
|
|
<h3>
|
|
|
|
|
|
编辑与展开{asset && ` · v${asset.version}`}
|
|
|
|
|
|
{dirty && " · 未保存"}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TemplateEditor
|
|
|
|
|
|
value={template}
|
|
|
|
|
|
onChange={setTemplate}
|
|
|
|
|
|
inputs={selectedInputs}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{asset && (
|
|
|
|
|
|
<div className="research-toolbar">
|
|
|
|
|
|
<span>版本记录</span>
|
|
|
|
|
|
<ResearchSelect
|
|
|
|
|
|
label="模板历史版本"
|
|
|
|
|
|
value={asset.version}
|
|
|
|
|
|
optionList={Array.from(
|
|
|
|
|
|
{
|
|
|
|
|
|
length:
|
|
|
|
|
|
assets.find((item) => item.id === asset.id)
|
|
|
|
|
|
?.version || asset.version,
|
|
|
|
|
|
},
|
|
|
|
|
|
(_, i) => ({ value: i + 1, label: `v${i + 1}` }),
|
|
|
|
|
|
)}
|
|
|
|
|
|
onChange={(version) =>
|
|
|
|
|
|
void task("读取历史版本", async () => {
|
|
|
|
|
|
const next = await api<Asset>(
|
|
|
|
|
|
`/research/assets/${asset.id}?version=${version}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
setAsset(next);
|
|
|
|
|
|
setTemplate(next.content);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setAsset(null);
|
|
|
|
|
|
setTemplate({
|
|
|
|
|
|
...template,
|
|
|
|
|
|
name: `${template.name}(副本)`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
另存新模板
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="research-toolbar">
|
2026-09-08 21:29:21 +08:00
|
|
|
|
<Button
|
2026-09-11 23:21:08 +08:00
|
|
|
|
disabled={!!busy || asset?.archived}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
onClick={() => void task("保存模板", save)}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
保存模板版本
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</Button>
|
2026-09-11 23:21:08 +08:00
|
|
|
|
{asset && !asset.archived && (
|
|
|
|
|
|
<DeleteResearchButton
|
|
|
|
|
|
name={asset.name}
|
|
|
|
|
|
label="模板"
|
|
|
|
|
|
disabled={!!busy}
|
|
|
|
|
|
onConfirm={() =>
|
|
|
|
|
|
task("删除模板", () => removeAsset(asset))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
<span>组合规模:{combination}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="research-form-grid">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
展开方式
|
|
|
|
|
|
<ResearchSelect
|
|
|
|
|
|
label="展开方式"
|
|
|
|
|
|
value={mode}
|
|
|
|
|
|
optionList={[
|
|
|
|
|
|
{ value: "all", label: "全组合" },
|
|
|
|
|
|
{ value: "random", label: "可复现随机采样" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(value) => setMode(value as string)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
候选数量上限
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
aria-label="候选数量上限"
|
|
|
|
|
|
value={limit}
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={10000}
|
|
|
|
|
|
onChange={(value) => setLimit(Number(value))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
随机种子
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
aria-label="随机种子"
|
|
|
|
|
|
value={seed}
|
|
|
|
|
|
onChange={(value) => setSeed(Number(value))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-09-12 12:11:53 +08:00
|
|
|
|
<SimulationSettingsEditor
|
|
|
|
|
|
value={settings}
|
|
|
|
|
|
onChange={setSettings}
|
|
|
|
|
|
{...options}
|
|
|
|
|
|
onRefresh={() => void options.refresh()}
|
|
|
|
|
|
fixedScopes={selectedInputs.map((r) => r.scope)}
|
|
|
|
|
|
disabled={!!busy}
|
|
|
|
|
|
onValidityChange={setSettingsValid}
|
|
|
|
|
|
/>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
<Button
|
2026-09-09 09:29:30 +08:00
|
|
|
|
theme="solid"
|
2026-09-12 12:11:53 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
dirty ||
|
|
|
|
|
|
!inputIds.length ||
|
|
|
|
|
|
!hypothesis ||
|
|
|
|
|
|
!!busy ||
|
|
|
|
|
|
!settingsValid
|
|
|
|
|
|
}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
onClick={() => void task("生成候选", expand)}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
保存候选研究记录
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</Button>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<section className="research-card">
|
|
|
|
|
|
<h3>目标范围检查</h3>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
保留种子表达式和模拟参数,按各目标输入的市场、股票池与延迟生成设置变体。局部变量不会当作字段;无法核实的字段不会被跳过。
|
|
|
|
|
|
</p>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
<Button
|
2026-09-09 09:29:30 +08:00
|
|
|
|
theme="solid"
|
|
|
|
|
|
disabled={!parent || !inputIds.length || !!busy}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void task("检查设置变体", async () => {
|
|
|
|
|
|
setExperiment(
|
|
|
|
|
|
await post("/research/variants/settings", {
|
|
|
|
|
|
alpha_id: parent,
|
2026-09-12 01:24:02 +08:00
|
|
|
|
...researchSelection(inputIds, inputs),
|
2026-09-09 09:29:30 +08:00
|
|
|
|
...(hypothesis ? { hypothesis } : {}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
生成设置变体
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</Button>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{experiment && (
|
|
|
|
|
|
<ExperimentView
|
|
|
|
|
|
key={experiment.id}
|
|
|
|
|
|
experiment={experiment}
|
|
|
|
|
|
onAction={onAction}
|
2026-09-11 23:21:08 +08:00
|
|
|
|
deleting={!!busy}
|
|
|
|
|
|
onDelete={() =>
|
|
|
|
|
|
task("删除生成记录", () => removeExperiment(experiment))
|
|
|
|
|
|
}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{page === "templates" && (
|
|
|
|
|
|
<details className="research-card">
|
|
|
|
|
|
<summary>导入旧模板</summary>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
粘贴 cnhk 模板 JSON
|
|
|
|
|
|
或模板数组。先预览转换结果,同名模板不会自动覆盖。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
aria-label="导入模板 JSON"
|
|
|
|
|
|
value={importText}
|
|
|
|
|
|
onChange={(text) => {
|
|
|
|
|
|
setImportText(text);
|
|
|
|
|
|
setImportResult(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
autosize={{ minRows: 4, maxRows: 12 }}
|
|
|
|
|
|
/>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
<Button
|
2026-09-09 09:29:30 +08:00
|
|
|
|
disabled={!!busy || !importText}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void task("预览导入", async () => {
|
|
|
|
|
|
const data = JSON.parse(importText);
|
|
|
|
|
|
setImportResult(
|
|
|
|
|
|
await post("/research/templates/import-preview", {
|
|
|
|
|
|
templates: Array.isArray(data) ? data : [data],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-09-08 21:29:21 +08:00
|
|
|
|
>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
预览导入差异
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</Button>
|
2026-09-09 09:29:30 +08:00
|
|
|
|
{importResult && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<pre>{JSON.stringify(importResult, null, 2)}</pre>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
!!busy ||
|
|
|
|
|
|
!!importResult.errors?.length ||
|
|
|
|
|
|
!!importResult.conflicts?.length
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void task("导入模板", async () => {
|
|
|
|
|
|
await post("/research/templates/import", {
|
|
|
|
|
|
templates: importResult.templates,
|
|
|
|
|
|
digest: importResult.digest,
|
|
|
|
|
|
});
|
|
|
|
|
|
setImportResult(null);
|
|
|
|
|
|
setImportText("");
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
Toast.success("导入完成");
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
确认导入
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
)}
|
2026-09-09 09:29:30 +08:00
|
|
|
|
</details>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{page === "variants" && (
|
|
|
|
|
|
<ComparisonPanel key={parent} baseline={parent} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ResearchDetail>
|
2026-09-08 21:29:21 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|