feat(research): separate template editing and backtest preparation tabs
Deploy production / deploy (push) Successful in 36s
Deploy production / deploy (push) Successful in 36s
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Table,
|
Table,
|
||||||
|
Tabs,
|
||||||
TextArea,
|
TextArea,
|
||||||
Toast,
|
Toast,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -56,6 +57,7 @@ export function ResearchWorkspace({
|
|||||||
onAction: (action: UIAction) => void;
|
onAction: (action: UIAction) => void;
|
||||||
onContext: (context: PageContext) => void;
|
onContext: (context: PageContext) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [detailTab, setDetailTab] = useState("editor");
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [detailMode, setDetailMode] = useState<"editor" | "experiment">(
|
const [detailMode, setDetailMode] = useState<"editor" | "experiment">(
|
||||||
"editor",
|
"editor",
|
||||||
@@ -152,6 +154,7 @@ export function ResearchWorkspace({
|
|||||||
["open_experiment", "open_template", "open_variant"].includes(action.type)
|
["open_experiment", "open_template", "open_variant"].includes(action.type)
|
||||||
) {
|
) {
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
|
setDetailTab("editor");
|
||||||
setDetailMode(
|
setDetailMode(
|
||||||
action.type === "open_experiment" ? "experiment" : "editor",
|
action.type === "open_experiment" ? "experiment" : "editor",
|
||||||
);
|
);
|
||||||
@@ -263,6 +266,7 @@ export function ResearchWorkspace({
|
|||||||
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
|
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
|
||||||
});
|
});
|
||||||
setExperiment(next);
|
setExperiment(next);
|
||||||
|
setDetailTab("prepare");
|
||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
const combination = Object.values(template.variables)
|
const combination = Object.values(template.variables)
|
||||||
@@ -310,6 +314,7 @@ export function ResearchWorkspace({
|
|||||||
setAsset(null);
|
setAsset(null);
|
||||||
setTemplate(blankTemplate());
|
setTemplate(blankTemplate());
|
||||||
setExperiment(null);
|
setExperiment(null);
|
||||||
|
setDetailTab("editor");
|
||||||
setDetailMode("editor");
|
setDetailMode("editor");
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -381,6 +386,7 @@ export function ResearchWorkspace({
|
|||||||
setAsset(next);
|
setAsset(next);
|
||||||
setTemplate(next.content);
|
setTemplate(next.content);
|
||||||
setExperiment(null);
|
setExperiment(null);
|
||||||
|
setDetailTab("editor");
|
||||||
setDetailMode("editor");
|
setDetailMode("editor");
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
})
|
})
|
||||||
@@ -541,247 +547,339 @@ export function ResearchWorkspace({
|
|||||||
description="此模板已删除,当前显示保留的历史版本;可另存新模板继续编辑。"
|
description="此模板已删除,当前显示保留的历史版本;可另存新模板继续编辑。"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<section className="research-card">
|
{page === "templates" && (
|
||||||
<h3>研究输入与假设</h3>
|
<Tabs
|
||||||
{page === "variants" && (
|
type="card"
|
||||||
<>
|
activeKey={detailTab}
|
||||||
<div className="research-methods">
|
onChange={setDetailTab}
|
||||||
<Button
|
tabPaneMotion={false}
|
||||||
theme={method === "structure" ? "solid" : "light"}
|
tabList={[
|
||||||
onClick={() => setMethod("structure")}
|
{ itemKey: "editor", tab: "模板信息编辑" },
|
||||||
>
|
{ itemKey: "prepare", tab: "回测准备" },
|
||||||
结构与字段变体
|
]}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
)}
|
||||||
theme={method === "settings" ? "solid" : "light"}
|
<div hidden={page === "templates" && detailTab !== "editor"}>
|
||||||
onClick={() => setMethod("settings")}
|
<section className="research-card">
|
||||||
>
|
<h3>研究输入与假设</h3>
|
||||||
市场与设置变体
|
{page === "variants" && (
|
||||||
</Button>
|
<>
|
||||||
</div>
|
<div className="research-methods">
|
||||||
<p>
|
<Button
|
||||||
{method === "structure"
|
theme={method === "structure" ? "solid" : "light"}
|
||||||
? "神笔马良:保留种子与改动依据,研究新的表达式。"
|
onClick={() => setMethod("structure")}
|
||||||
: "缘分一道桥:保持表达式,为各目标市场重新固定输入后检查可用范围。"}
|
>
|
||||||
</p>
|
结构与字段变体
|
||||||
<label>
|
</Button>
|
||||||
种子 Alpha
|
<Button
|
||||||
<Input
|
theme={method === "settings" ? "solid" : "light"}
|
||||||
aria-label="种子 Alpha"
|
onClick={() => setMethod("settings")}
|
||||||
value={parent}
|
>
|
||||||
onChange={setParent}
|
市场与设置变体
|
||||||
placeholder="已同步的 Alpha ID"
|
</Button>
|
||||||
/>
|
</div>
|
||||||
</label>
|
<p>
|
||||||
</>
|
{method === "structure"
|
||||||
)}
|
? "神笔马良:保留种子与改动依据,研究新的表达式。"
|
||||||
<label>
|
: "缘分一道桥:保持表达式,为各目标市场重新固定输入后检查可用范围。"}
|
||||||
固定研究输入
|
</p>
|
||||||
<ResearchDataInput
|
<label>
|
||||||
label="选择数据准备"
|
种子 Alpha
|
||||||
ids={inputIds}
|
<Input
|
||||||
inputs={inputs}
|
aria-label="种子 Alpha"
|
||||||
onChange={(ids, rows) => {
|
value={parent}
|
||||||
setInputs(rows);
|
onChange={setParent}
|
||||||
setInputIds(ids);
|
placeholder="已同步的 Alpha ID"
|
||||||
const first = rows.find((r) => r.id === ids[0]);
|
/>
|
||||||
if (first)
|
</label>
|
||||||
setSettings((old) => ({
|
</>
|
||||||
...old,
|
)}
|
||||||
region: first.scope.region,
|
|
||||||
universe: first.scope.universe,
|
|
||||||
delay: first.scope.delay,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<p className="research-hint">
|
|
||||||
从数据准备选择集合;每个集合可包含同范围下多个数据集的字段。
|
|
||||||
</p>
|
|
||||||
{page === "variants" && method === "settings" ? (
|
|
||||||
<label>
|
<label>
|
||||||
本次研究假设
|
固定研究输入
|
||||||
<TextArea
|
<ResearchDataInput
|
||||||
aria-label="本次研究假设"
|
label="选择数据准备"
|
||||||
value={hypothesis}
|
ids={inputIds}
|
||||||
onChange={setHypothesis}
|
inputs={inputs}
|
||||||
autosize={{ minRows: 2, maxRows: 5 }}
|
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,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
) : (
|
<p className="research-hint">
|
||||||
<div className="research-ai-section">
|
从数据准备选择集合;每个集合可包含同范围下多个数据集的字段。
|
||||||
<h4>
|
</p>
|
||||||
<IconAIEditLevel3 aria-hidden="true" />
|
{page === "variants" && method === "settings" ? (
|
||||||
|
<label>
|
||||||
本次研究假设
|
本次研究假设
|
||||||
</h4>
|
<TextArea
|
||||||
<ResearchInput
|
aria-label="本次研究假设"
|
||||||
maxLength={10000}
|
value={hypothesis}
|
||||||
label="本次研究假设"
|
onChange={setHypothesis}
|
||||||
value={hypothesis}
|
autosize={{ minRows: 2, maxRows: 5 }}
|
||||||
onChange={setHypothesis}
|
/>
|
||||||
placeholder={
|
</label>
|
||||||
page === "variants"
|
) : (
|
||||||
? "描述希望保留的信号与要探索的变体方向…"
|
<div className="research-ai-section">
|
||||||
: "描述经济假设、信号方向与希望验证的关系…"
|
<h4>
|
||||||
}
|
<IconAIEditLevel3 aria-hidden="true" />
|
||||||
references={[
|
本次研究假设
|
||||||
...selectedInputs.map((input) => ({
|
</h4>
|
||||||
id: input.id,
|
<ResearchInput
|
||||||
name: `${input.name} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id}`,
|
maxLength={10000}
|
||||||
})),
|
label="本次研究假设"
|
||||||
...(page === "variants" && parent
|
value={hypothesis}
|
||||||
? [
|
onChange={setHypothesis}
|
||||||
{
|
placeholder={
|
||||||
id: "parent-alpha",
|
page === "variants"
|
||||||
name: `种子 Alpha:${parent}`,
|
? "描述希望保留的信号与要探索的变体方向…"
|
||||||
},
|
: "描述经济假设、信号方向与希望验证的关系…"
|
||||||
]
|
}
|
||||||
: []),
|
references={[
|
||||||
]}
|
...selectedInputs.map((input) => ({
|
||||||
disabled={!!busy}
|
id: input.id,
|
||||||
canSubmit={!!inputIds.length}
|
name: `${input.name} · ${input.scope.region}/${input.scope.universe}/D${input.scope.delay} · ${input.field_ids.length} 字段 · ${input.id}`,
|
||||||
busy={busy === "AI 生成"}
|
})),
|
||||||
submitLabel="AI 生成模板"
|
...(page === "variants" && parent
|
||||||
sendHotKey="shift+enter"
|
? [
|
||||||
onSubmit={() => void task("AI 生成", generate)}
|
{
|
||||||
prompts={
|
id: "parent-alpha",
|
||||||
page === "variants"
|
name: `种子 Alpha:${parent}`,
|
||||||
? ["保留种子的经济含义,探索不同时间窗口和字段组合"]
|
},
|
||||||
: ["研究短期价格反转与成交量之间的关系"]
|
]
|
||||||
}
|
: []),
|
||||||
hint={
|
]}
|
||||||
inputIds.length
|
disabled={!!busy}
|
||||||
? "基于已关联的固定输入生成,保留假设供下一轮调整。"
|
canSubmit={!!inputIds.length}
|
||||||
: "请先关联固定研究输入。"
|
busy={busy === "AI 生成"}
|
||||||
}
|
submitLabel="AI 生成模板"
|
||||||
/>
|
sendHotKey="shift+enter"
|
||||||
</div>
|
onSubmit={() => void task("AI 生成", generate)}
|
||||||
)}
|
prompts={
|
||||||
</section>
|
page === "variants"
|
||||||
{page !== "variants" || method === "structure" ? (
|
? ["保留种子的经济含义,探索不同时间窗口和字段组合"]
|
||||||
<section className="research-card">
|
: ["研究短期价格反转与成交量之间的关系"]
|
||||||
<div className="research-section-heading">
|
}
|
||||||
<h3>
|
hint={
|
||||||
编辑与展开{asset && ` · v${asset.version}`}
|
inputIds.length
|
||||||
{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>
|
||||||
)}
|
)}
|
||||||
<div className="research-toolbar">
|
|
||||||
<Button
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
<SimulationSettingsEditor
|
|
||||||
value={settings}
|
|
||||||
onChange={setSettings}
|
|
||||||
{...options}
|
|
||||||
onRefresh={() => void options.refresh()}
|
|
||||||
fixedScopes={selectedInputs.map((r) => r.scope)}
|
|
||||||
disabled={!!busy}
|
|
||||||
onValidityChange={setSettingsValid}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
theme="solid"
|
|
||||||
disabled={
|
|
||||||
dirty ||
|
|
||||||
!inputIds.length ||
|
|
||||||
!hypothesis ||
|
|
||||||
!!busy ||
|
|
||||||
!settingsValid
|
|
||||||
}
|
|
||||||
onClick={() => void task("生成候选", expand)}
|
|
||||||
>
|
|
||||||
保存候选研究记录
|
|
||||||
</Button>
|
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
{page !== "variants" || method === "structure" ? (
|
||||||
|
<>
|
||||||
|
<div hidden={page === "templates" && detailTab !== "editor"}>
|
||||||
|
<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">
|
||||||
|
<Button
|
||||||
|
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>
|
||||||
|
{page === "templates" && (
|
||||||
|
<Button
|
||||||
|
theme="solid"
|
||||||
|
disabled={!!busy || dirty}
|
||||||
|
onClick={() => setDetailTab("prepare")}
|
||||||
|
>
|
||||||
|
下一步:回测准备
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div hidden={page === "templates" && detailTab !== "prepare"}>
|
||||||
|
<section className="research-card">
|
||||||
|
<h3>回测准备</h3>
|
||||||
|
<p className="research-hint">
|
||||||
|
配置展开方式与模拟设置 → 生成候选研究集合 →
|
||||||
|
生成回测确认预览 → 确认后开始回测。
|
||||||
|
</p>
|
||||||
|
{page === "templates" && (
|
||||||
|
<>
|
||||||
|
<label>
|
||||||
|
固定研究输入
|
||||||
|
<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,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
本次研究假设
|
||||||
|
<TextArea
|
||||||
|
aria-label="回测研究假设"
|
||||||
|
value={hypothesis}
|
||||||
|
onChange={setHypothesis}
|
||||||
|
autosize={{ minRows: 2, maxRows: 5 }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
模板:{template.name || "未命名"} ·{" "}
|
||||||
|
{asset ? `v${asset.version}` : "未保存"} · 组合规模:
|
||||||
|
{combination}
|
||||||
|
</p>
|
||||||
|
{dirty && (
|
||||||
|
<Banner
|
||||||
|
type="warning"
|
||||||
|
description="请返回模板信息编辑,保存模板版本后再生成候选。"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
<SimulationSettingsEditor
|
||||||
|
value={settings}
|
||||||
|
onChange={setSettings}
|
||||||
|
{...options}
|
||||||
|
onRefresh={() => void options.refresh()}
|
||||||
|
fixedScopes={selectedInputs.map((r) => r.scope)}
|
||||||
|
disabled={!!busy}
|
||||||
|
onValidityChange={setSettingsValid}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
theme="solid"
|
||||||
|
disabled={
|
||||||
|
dirty ||
|
||||||
|
!inputIds.length ||
|
||||||
|
!hypothesis ||
|
||||||
|
!!busy ||
|
||||||
|
!settingsValid
|
||||||
|
}
|
||||||
|
onClick={() => void task("生成候选", expand)}
|
||||||
|
>
|
||||||
|
生成候选研究集合
|
||||||
|
</Button>
|
||||||
|
{(!inputIds.length || !hypothesis || !settingsValid) && (
|
||||||
|
<p className="research-hint">
|
||||||
|
待完成:
|
||||||
|
{[
|
||||||
|
!inputIds.length && "选择固定研究输入",
|
||||||
|
!hypothesis && "填写研究假设",
|
||||||
|
!settingsValid && "检查模拟设置",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("、")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<section className="research-card">
|
<section className="research-card">
|
||||||
<h3>目标范围检查</h3>
|
<h3>目标范围检查</h3>
|
||||||
@@ -808,18 +906,20 @@ export function ResearchWorkspace({
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{experiment && (
|
<div hidden={page === "templates" && detailTab !== "prepare"}>
|
||||||
<ExperimentView
|
{experiment && (
|
||||||
key={experiment.id}
|
<ExperimentView
|
||||||
experiment={experiment}
|
key={experiment.id}
|
||||||
onAction={onAction}
|
experiment={experiment}
|
||||||
deleting={!!busy}
|
onAction={onAction}
|
||||||
onDelete={() =>
|
deleting={!!busy}
|
||||||
task("删除生成记录", () => removeExperiment(experiment))
|
onDelete={() =>
|
||||||
}
|
task("删除生成记录", () => removeExperiment(experiment))
|
||||||
/>
|
}
|
||||||
)}
|
/>
|
||||||
{page === "templates" && (
|
)}
|
||||||
|
</div>
|
||||||
|
{page === "templates" && detailTab === "editor" && (
|
||||||
<details className="research-card">
|
<details className="research-card">
|
||||||
<summary>导入旧模板</summary>
|
<summary>导入旧模板</summary>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole("textbox", { name: "模板名称", exact: true }),
|
page.getByRole("textbox", { name: "模板名称", exact: true }),
|
||||||
).toHaveValue("特征转换模板");
|
).toHaveValue("特征转换模板");
|
||||||
|
await page.getByRole("tab", { name: "回测准备", exact: true }).click();
|
||||||
const settingsEditor = page.getByRole("region", {
|
const settingsEditor = page.getByRole("region", {
|
||||||
name: "回测参数",
|
name: "回测参数",
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ for (const mode of ["templates", "variants", "features"] as const) {
|
|||||||
const hypothesis = "检验 <收益> 与规模\n第二轮保留原始假设";
|
const hypothesis = "检验 <收益> 与规模\n第二轮保留原始假设";
|
||||||
await input.fill(hypothesis);
|
await input.fill(hypothesis);
|
||||||
await expect(
|
await expect(
|
||||||
sheet.locator("p").filter({ hasText: /Semi 准备集合 ·/ }),
|
sheet.locator("p:visible").filter({ hasText: /Semi 准备集合 ·/ }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
let release!: () => void;
|
let release!: () => void;
|
||||||
const pending = new Promise<void>((resolve) => {
|
const pending = new Promise<void>((resolve) => {
|
||||||
@@ -148,6 +148,22 @@ for (const mode of ["templates", "variants", "features"] as const) {
|
|||||||
sheet.getByRole("textbox", { name: "模板名称", exact: true }),
|
sheet.getByRole("textbox", { name: "模板名称", exact: true }),
|
||||||
).toHaveValue("AI 生成验收");
|
).toHaveValue("AI 生成验收");
|
||||||
await expect(generate).toBeEnabled();
|
await expect(generate).toBeEnabled();
|
||||||
|
if (mode === "templates") {
|
||||||
|
await sheet.getByRole("button", { name: "下一步:回测准备" }).click();
|
||||||
|
await expect(
|
||||||
|
sheet.getByRole("textbox", { name: "回测研究假设", exact: true }),
|
||||||
|
).toHaveValue(hypothesis);
|
||||||
|
await expect(
|
||||||
|
sheet.getByRole("button", { name: "生成候选研究集合", exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(generate).toBeHidden();
|
||||||
|
await sheet
|
||||||
|
.getByRole("tab", { name: "模板信息编辑", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
sheet.getByRole("textbox", { name: "模板名称", exact: true }),
|
||||||
|
).toHaveValue("AI 生成验收");
|
||||||
|
}
|
||||||
await expect(input).toContainText("检验 <收益> 与规模");
|
await expect(input).toContainText("检验 <收益> 与规模");
|
||||||
await page.screenshot({ path: `../output/playwright/semi-ai-${mode}.png` });
|
await page.screenshot({ path: `../output/playwright/semi-ai-${mode}.png` });
|
||||||
await page.setViewportSize({ width: 390, height: 900 });
|
await page.setViewportSize({ width: 390, height: 900 });
|
||||||
|
|||||||
Reference in New Issue
Block a user