Simplify template candidate confirmation and direct batch backtesting
Deploy production / deploy (push) Successful in 57s
Deploy production / deploy (push) Successful in 57s
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { useState } from "react";
|
||||
import { Pagination } from "@douyinfe/semi-ui-19";
|
||||
import { WorkspaceTable } from "../components/WorkspaceTable";
|
||||
import type { ResearchCandidate } from "./workspaceTypes";
|
||||
|
||||
/** Selection spans pages; eligibility is supplied by the producing workflow. */
|
||||
export function CandidateTable({
|
||||
candidates,
|
||||
selected,
|
||||
onSelectionChange,
|
||||
disabled = false,
|
||||
requireValidation = false,
|
||||
}: {
|
||||
candidates: ResearchCandidate[];
|
||||
selected: string[];
|
||||
onSelectionChange: (ids: string[]) => void;
|
||||
disabled?: boolean;
|
||||
requireValidation?: boolean;
|
||||
}) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
return (
|
||||
<>
|
||||
<WorkspaceTable<ResearchCandidate>
|
||||
dataSource={candidates.slice((page - 1) * pageSize, page * pageSize)}
|
||||
className="research-candidate-table"
|
||||
rowKey="client_item_id"
|
||||
scroll={{ x: 1120, y: 400 }}
|
||||
empty="暂无候选表达式。"
|
||||
rowSelection={{
|
||||
width: 48,
|
||||
disabled,
|
||||
selectedRowKeys: selected,
|
||||
onChange: (keys) => onSelectionChange((keys ?? []).map(String)),
|
||||
getCheckboxProps: (candidate) => ({
|
||||
disabled:
|
||||
disabled ||
|
||||
(requireValidation && candidate.validation?.status !== "valid"),
|
||||
"aria-label": `选择候选 ${candidate.client_item_id}`,
|
||||
title: requireValidation
|
||||
? [
|
||||
...(candidate.validation?.syntax ?? []),
|
||||
...(candidate.validation?.types ?? []),
|
||||
...(candidate.validation?.availability ?? []),
|
||||
].join(";")
|
||||
: undefined,
|
||||
}),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: "表达式",
|
||||
dataIndex: "expression",
|
||||
width: 420,
|
||||
render: (value) => <code title={value}>{value}</code>,
|
||||
},
|
||||
...(
|
||||
[
|
||||
["neutralization", "Neutralization", 170],
|
||||
["decay", "Decay", 90],
|
||||
["truncation", "Truncation", 120],
|
||||
["maxTrade", "Max Trade", 120],
|
||||
["nanHandling", "NaN Handling", 150],
|
||||
] as const
|
||||
).map(([key, title, width]) => ({
|
||||
key,
|
||||
title,
|
||||
width,
|
||||
render: (_: unknown, candidate: ResearchCandidate) =>
|
||||
candidate.settings[key],
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<div className="table-pagination workspace-table-footer">
|
||||
<span>
|
||||
共 {candidates.length} 个候选 · 已选 {selected.length} 个
|
||||
</span>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
pageSize={pageSize}
|
||||
total={candidates.length}
|
||||
showSizeChanger
|
||||
pageSizeOpts={[25, 50, 100]}
|
||||
preventPageChangeOnPageSizeChange
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Checkbox,
|
||||
Table,
|
||||
Tag,
|
||||
Toast,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
import { Banner, Button, Toast } from "@douyinfe/semi-ui-19";
|
||||
import { post, formatTime } from "../api";
|
||||
import type { UIAction } from "../ai/types";
|
||||
import type { Experiment } from "./workspaceTypes";
|
||||
import { EvaluationPanel } from "./EvaluationPanel";
|
||||
import { LineagePanel } from "./LineagePanel";
|
||||
import { ResearchSelect } from "./ResearchSelect";
|
||||
import { validationLabel } from "./workspaceTypes";
|
||||
import { CandidateTable } from "./CandidateTable";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
|
||||
export function ExperimentView({
|
||||
@@ -33,7 +26,7 @@ export function ExperimentView({
|
||||
const [runId, setRunId] = useState(experiment.backtest_run_ids[0]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const valid = experiment.candidates.filter(
|
||||
(c) => c.validation.status === "valid",
|
||||
(c) => c.validation?.status === "valid",
|
||||
);
|
||||
async function preview() {
|
||||
setBusy(true);
|
||||
@@ -72,7 +65,10 @@ export function ExperimentView({
|
||||
生成回测确认预览{selected.length ? `(${selected.length})` : ""}
|
||||
</Button>
|
||||
</div>
|
||||
<p>{experiment.hypothesis}</p>
|
||||
<details>
|
||||
<summary>研究描述</summary>
|
||||
<p>{experiment.hypothesis}</p>
|
||||
</details>
|
||||
{experiment.archived ? (
|
||||
<Banner
|
||||
type="info"
|
||||
@@ -92,175 +88,12 @@ export function ExperimentView({
|
||||
type="info"
|
||||
description="候选已保存为不可变研究记录。本地校验不保证平台可执行;在回测预览中确认后才开始模拟。"
|
||||
/>
|
||||
<div className="research-table-scroll">
|
||||
<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>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
bindings: candidate.bindings,
|
||||
changes: candidate.changes,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</details>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
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}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "校验",
|
||||
key: "validation",
|
||||
width: 180,
|
||||
render: (_, candidate) => (
|
||||
<>
|
||||
<Tag
|
||||
color={
|
||||
candidate.validation.status === "valid"
|
||||
? "green"
|
||||
: "orange"
|
||||
}
|
||||
>
|
||||
{validationLabel[candidate.validation.status]}
|
||||
</Tag>
|
||||
{(["syntax", "types", "availability"] as const).map((key) =>
|
||||
candidate.validation[key].map((issue, index) => (
|
||||
<p key={`${key}-${index}`}>
|
||||
{
|
||||
{
|
||||
syntax: "语法",
|
||||
types: "类型",
|
||||
availability: "可用性",
|
||||
}[key]
|
||||
}
|
||||
:{issue}
|
||||
</p>
|
||||
)),
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="research-lineage">
|
||||
<strong>研究来源</strong>
|
||||
<span>实验 {experiment.id}</span>
|
||||
{experiment.evidence.template?.id && (
|
||||
<Button
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
onAction({
|
||||
type: "open_template",
|
||||
asset_id: experiment.evidence.template!.id,
|
||||
version: experiment.evidence.template!.version,
|
||||
nonce: Date.now(),
|
||||
})
|
||||
}
|
||||
>
|
||||
模板 v{experiment.evidence.template.version}
|
||||
</Button>
|
||||
)}
|
||||
{experiment.inputs.map((input) => (
|
||||
<Button
|
||||
key={input.id}
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
onAction({
|
||||
type: "open_research_input",
|
||||
input_id: input.id,
|
||||
nonce: Date.now(),
|
||||
})
|
||||
}
|
||||
>
|
||||
{input.name} · {input.scope.region}/{input.scope.universe}/D
|
||||
{input.scope.delay}
|
||||
</Button>
|
||||
))}
|
||||
{experiment.parents.map((parent) => (
|
||||
<Button
|
||||
key={`${parent.kind}:${parent.id}`}
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
onAction(
|
||||
parent.kind === "alpha"
|
||||
? {
|
||||
type: "open_alpha",
|
||||
alpha_id: parent.id,
|
||||
nonce: Date.now(),
|
||||
}
|
||||
: {
|
||||
type: "open_experiment",
|
||||
experiment_id: parent.id,
|
||||
nonce: Date.now(),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
父来源:{parent.id}
|
||||
</Button>
|
||||
))}
|
||||
{experiment.backtest_run_ids.map((id) => (
|
||||
<Button
|
||||
key={id}
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
onAction({ type: "open_backtest", run_id: id, nonce: Date.now() })
|
||||
}
|
||||
>
|
||||
查看关联回测
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<CandidateTable
|
||||
candidates={experiment.candidates}
|
||||
selected={selected}
|
||||
onSelectionChange={setSelected}
|
||||
requireValidation
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<Button onClick={() => setAssessment((v) => !v)}>评估研究结果</Button>
|
||||
<Button onClick={() => setRelations((v) => !v)}>查看变体关系</Button>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "./workspaceTypes";
|
||||
import { TemplateEditor } from "./TemplateEditor";
|
||||
import { ExperimentView } from "./ExperimentView";
|
||||
import { TemplateCandidateSet } from "./TemplateCandidateSet";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
import { ComparisonPanel } from "./ComparisonPanel";
|
||||
import "./workspace.css";
|
||||
@@ -83,9 +84,27 @@ export function ResearchWorkspace({
|
||||
const [assetTotal, setAssetTotal] = useState(0);
|
||||
const [historyTotal, setHistoryTotal] = useState(0);
|
||||
const refreshSequence = useRef(0);
|
||||
const CandidateView =
|
||||
experiment?.kind === "template" ? TemplateCandidateSet : ExperimentView;
|
||||
const selectedInputs = inputs.filter((input) => inputIds.includes(input.id));
|
||||
const dirty =
|
||||
!asset || JSON.stringify(template) !== JSON.stringify(asset.content);
|
||||
const generationKey = JSON.stringify([
|
||||
asset?.id,
|
||||
asset?.version,
|
||||
template,
|
||||
inputIds,
|
||||
inputs,
|
||||
settings,
|
||||
mode,
|
||||
limit,
|
||||
seed,
|
||||
]);
|
||||
const currentGeneration = useRef(generationKey);
|
||||
currentGeneration.current = generationKey;
|
||||
useEffect(() => {
|
||||
if (page === "templates" && detailMode === "editor") setExperiment(null);
|
||||
}, [page, template, inputIds, inputs, settings, mode, limit, seed]);
|
||||
async function task(label: string, action: () => Promise<void>) {
|
||||
setBusy(label);
|
||||
setError("");
|
||||
@@ -99,13 +118,12 @@ export function ResearchWorkspace({
|
||||
}
|
||||
async function refresh() {
|
||||
const sequence = ++refreshSequence.current;
|
||||
const [nextAssets, nextInputs, nextHistory] = await Promise.all([
|
||||
const [nextAssets, nextHistory] = await Promise.all([
|
||||
api<{ items: Asset[]; total: number }>(
|
||||
`/research/assets?kind=template&limit=25&offset=${assetPage * 25}&q=${encodeURIComponent(search)}`,
|
||||
),
|
||||
Promise.resolve({ items: inputs }),
|
||||
api<{ items: typeof history; total: number }>(
|
||||
`/research/experiments?limit=25&offset=${historyPage * 25}`,
|
||||
`/research/experiments?kind=${page === "templates" ? "template" : "variant"}&limit=25&offset=${historyPage * 25}`,
|
||||
),
|
||||
]);
|
||||
if (sequence !== refreshSequence.current) return;
|
||||
@@ -118,7 +136,6 @@ export function ResearchWorkspace({
|
||||
Math.min(current, Math.max(0, Math.ceil(nextHistory.total / 25) - 1)),
|
||||
);
|
||||
setAssets(nextAssets.items);
|
||||
setInputs(nextInputs.items);
|
||||
setHistory(nextHistory.items);
|
||||
}
|
||||
useEffect(() => {
|
||||
@@ -261,6 +278,9 @@ export function ResearchWorkspace({
|
||||
seed,
|
||||
parent_alpha_ids: parent ? parent.split(/[,,\s]+/).filter(Boolean) : [],
|
||||
});
|
||||
if (page === "templates" && currentGeneration.current !== generationKey) {
|
||||
throw new Error("回测准备已改变,请按当前配置重新生成候选集合");
|
||||
}
|
||||
setExperiment(next);
|
||||
setDetailTab("prepare");
|
||||
await refresh();
|
||||
@@ -527,7 +547,7 @@ export function ResearchWorkspace({
|
||||
{error && <Banner type="danger" description={error} />}
|
||||
{detailMode === "experiment" ? (
|
||||
experiment && (
|
||||
<ExperimentView
|
||||
<CandidateView
|
||||
key={experiment.id}
|
||||
experiment={experiment}
|
||||
onAction={onAction}
|
||||
@@ -552,7 +572,7 @@ export function ResearchWorkspace({
|
||||
onChange={setDetailTab}
|
||||
tabPaneMotion={false}
|
||||
tabList={[
|
||||
{ itemKey: "editor", tab: "模板信息编辑" },
|
||||
{ itemKey: "editor", tab: "模板详情" },
|
||||
{ itemKey: "prepare", tab: "回测准备" },
|
||||
]}
|
||||
/>
|
||||
@@ -807,7 +827,7 @@ export function ResearchWorkspace({
|
||||
{dirty && (
|
||||
<Banner
|
||||
type="warning"
|
||||
description="请返回模板信息编辑,保存模板版本后再生成候选。"
|
||||
description="请返回模板详情,保存模板版本后再生成候选。"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -845,6 +865,9 @@ export function ResearchWorkspace({
|
||||
</label>
|
||||
</div>
|
||||
<SimulationSettingsEditor
|
||||
validationMode={
|
||||
page === "templates" ? "combination" : "platform"
|
||||
}
|
||||
value={settings}
|
||||
onChange={setSettings}
|
||||
{...options}
|
||||
@@ -911,7 +934,7 @@ export function ResearchWorkspace({
|
||||
)}
|
||||
<div hidden={page === "templates" && detailTab !== "prepare"}>
|
||||
{experiment && (
|
||||
<ExperimentView
|
||||
<CandidateView
|
||||
key={experiment.id}
|
||||
experiment={experiment}
|
||||
onAction={onAction}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Banner, Button, Toast } from "@douyinfe/semi-ui-19";
|
||||
import { post } from "../api";
|
||||
import type { UIAction } from "../ai/types";
|
||||
import { CandidateTable } from "./CandidateTable";
|
||||
import { DeleteResearchButton } from "./DeleteResearchButton";
|
||||
import type { Experiment } from "./workspaceTypes";
|
||||
|
||||
/** The generated collection is the confirmation surface for a template backtest. */
|
||||
export function TemplateCandidateSet({
|
||||
experiment,
|
||||
onAction,
|
||||
onDelete,
|
||||
deleting,
|
||||
}: {
|
||||
experiment: Experiment;
|
||||
onAction: (action: UIAction) => void;
|
||||
onDelete?: () => Promise<void>;
|
||||
deleting?: boolean;
|
||||
}) {
|
||||
const [selected, setSelected] = useState(() =>
|
||||
experiment.candidates.map((c) => c.client_item_id),
|
||||
);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [runs, setRuns] = useState(experiment.backtest_run_ids);
|
||||
const attempt = useRef<{ selection: string; key: string } | null>(null);
|
||||
const pending = useRef(false);
|
||||
async function backtest() {
|
||||
if (pending.current || !selected.length) return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
// Retain the same request key after network failure: retry must not create a second run.
|
||||
const selection = JSON.stringify([...selected].sort());
|
||||
if (attempt.current?.selection !== selection) {
|
||||
attempt.current = { selection, key: crypto.randomUUID() };
|
||||
}
|
||||
try {
|
||||
const result = await post<{ backtest_run_id: string }>(
|
||||
`/research/experiments/${experiment.id}/backtest`,
|
||||
{
|
||||
candidate_ids: selected,
|
||||
idempotency_key: attempt.current.key,
|
||||
},
|
||||
);
|
||||
setRuns((old) => [...new Set([...old, result.backtest_run_id])]);
|
||||
onAction({
|
||||
type: "open_backtest",
|
||||
run_id: result.backtest_run_id,
|
||||
nonce: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
Toast.error((error as Error).message);
|
||||
} finally {
|
||||
pending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<section className="research-card" aria-label="回测候选集合">
|
||||
<h3>回测候选集合</h3>
|
||||
<details>
|
||||
<summary>研究描述</summary>
|
||||
<p>{experiment.hypothesis}</p>
|
||||
</details>
|
||||
{experiment.archived && (
|
||||
<Banner type="info" description="此候选集合已删除,保留历史关联。" />
|
||||
)}
|
||||
<p>确认表达式和参数后,点击“回测”将所选候选交给回测研究批量执行。</p>
|
||||
<CandidateTable
|
||||
candidates={experiment.candidates}
|
||||
selected={selected}
|
||||
onSelectionChange={setSelected}
|
||||
disabled={busy || deleting || experiment.archived}
|
||||
/>
|
||||
<div className="research-toolbar">
|
||||
<Button
|
||||
theme="solid"
|
||||
disabled={!selected.length || deleting || experiment.archived}
|
||||
loading={busy}
|
||||
onClick={() => void backtest()}
|
||||
>
|
||||
回测({selected.length})
|
||||
</Button>
|
||||
{onDelete && !experiment.archived && (
|
||||
<DeleteResearchButton
|
||||
name={experiment.name}
|
||||
label="候选集合"
|
||||
disabled={busy || deleting}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
)}
|
||||
{runs.map((id, index) => (
|
||||
<Button
|
||||
key={id}
|
||||
theme="borderless"
|
||||
onClick={() =>
|
||||
onAction({ type: "open_backtest", run_id: id, nonce: Date.now() })
|
||||
}
|
||||
>
|
||||
关联回测 {index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -395,3 +395,11 @@
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.research-card .research-candidate-table .semi-checkbox {
|
||||
margin: 0;
|
||||
flex-direction: row;
|
||||
}
|
||||
.research-candidate-table .workspace-cell code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export type InputSnapshot = {
|
||||
created_at: string;
|
||||
};
|
||||
export type ResearchCandidate = Candidate & {
|
||||
validation: {
|
||||
validation?: {
|
||||
status: string;
|
||||
syntax: string[];
|
||||
types: string[];
|
||||
|
||||
@@ -28,7 +28,9 @@ export function SimulationSettingsEditor({
|
||||
fixedScopes,
|
||||
disabled = false,
|
||||
onValidityChange,
|
||||
validationMode = "platform",
|
||||
}: {
|
||||
validationMode?: "platform" | "combination";
|
||||
alphaType?: "REGULAR" | "SUPER";
|
||||
value: SimulationSettings;
|
||||
onChange: (value: SimulationSettings) => void;
|
||||
@@ -43,18 +45,36 @@ export function SimulationSettingsEditor({
|
||||
}) {
|
||||
const scope = settingsScope(value);
|
||||
const row = rows.find((r) => sameScope(r, scope));
|
||||
const errors = settingsErrors(value, rows, fixedScopes);
|
||||
const combinationOnly = validationMode === "combination";
|
||||
const errors = combinationOnly
|
||||
? !fixedScopes?.length || fixedScopes.some((s) => !sameScope(s, scope))
|
||||
? ["所选数据准备必须具有相同组合,并与回测组合一致"]
|
||||
: []
|
||||
: settingsErrors(value, rows, fixedScopes);
|
||||
const [scopeReady, setScopeReady] = useState(false);
|
||||
const valid =
|
||||
!loading &&
|
||||
!error &&
|
||||
!errors.length &&
|
||||
(fixedScopes !== undefined || scopeReady);
|
||||
const valid = combinationOnly
|
||||
? !errors.length
|
||||
: !loading &&
|
||||
!error &&
|
||||
!errors.length &&
|
||||
(fixedScopes !== undefined || scopeReady);
|
||||
useEffect(() => {
|
||||
onValidityChange?.(valid);
|
||||
}, [valid, onValidityChange]);
|
||||
function field(key: string) {
|
||||
const options = fieldOptions(row, key);
|
||||
const options = combinationOnly
|
||||
? key === "neutralization"
|
||||
? {
|
||||
choices: [
|
||||
...new Set([
|
||||
value.neutralization,
|
||||
...(row?.neutralizations ??
|
||||
rows.flatMap((r) => r.neutralizations)),
|
||||
]),
|
||||
],
|
||||
}
|
||||
: fieldOptions(undefined, key)
|
||||
: fieldOptions(row, key);
|
||||
const current =
|
||||
value[key as keyof SimulationSettings] ??
|
||||
(key === "maxPosition" ? "OFF" : undefined);
|
||||
@@ -71,7 +91,7 @@ export function SimulationSettingsEditor({
|
||||
value={typeof current === "boolean" ? String(current) : current}
|
||||
disabled={
|
||||
disabled ||
|
||||
loading ||
|
||||
(loading && !combinationOnly) ||
|
||||
!options.choices.length ||
|
||||
(options.choices.length === 1 && !message)
|
||||
}
|
||||
@@ -97,7 +117,7 @@ export function SimulationSettingsEditor({
|
||||
? current
|
||||
: undefined
|
||||
}
|
||||
disabled={disabled || loading}
|
||||
disabled={disabled || (loading && !combinationOnly)}
|
||||
min={options.minimum}
|
||||
max={options.maximum}
|
||||
precision={
|
||||
@@ -129,16 +149,30 @@ export function SimulationSettingsEditor({
|
||||
选项更新时间:{formatTime(fetchedAt ?? null)}
|
||||
</small>
|
||||
{error && (
|
||||
<Banner type="warning" description={`${error};请重试同步合法设置`} />
|
||||
<Banner
|
||||
type="warning"
|
||||
description={
|
||||
combinationOnly
|
||||
? "参数选项暂不可用,可使用当前参数继续生成候选。"
|
||||
: `${error};请重试同步合法设置`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!loading && !rows.length && !error && (
|
||||
<Banner type="info" description="尚无参数选项,请先同步合法设置。" />
|
||||
<Banner
|
||||
type="info"
|
||||
description={
|
||||
combinationOnly
|
||||
? "可同步参数选项辅助选择,当前参数仍可用于生成候选。"
|
||||
: "尚无参数选项,请先同步合法设置。"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ScopePicker
|
||||
value={scope}
|
||||
options={asScopeOptions(rows)}
|
||||
readOnly={fixedScopes !== undefined}
|
||||
disabled={disabled || loading}
|
||||
disabled={disabled || (loading && !combinationOnly)}
|
||||
onValidityChange={setScopeReady}
|
||||
onChange={(next) =>
|
||||
onChange({
|
||||
|
||||
Reference in New Issue
Block a user