feat: 统一研究模块表格布局与详情交互
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
# 统一列表与详情交互
|
||||||
|
Type: task
|
||||||
|
Status: ready-for-agent
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
六个研究模块使用 table + detail,除 QuantFlow 独立详情页外均为 60% 右侧抽屉。所有列表页连续 Flex 高度链,移除页面顶部标题描述,按钮区位于筛选下方。
|
||||||
|
|
||||||
|
## Selected learning
|
||||||
|
采用 20260724-bounded-list-flex-height-chain:当前应用外壳已有有界 Flex,研究页面仍整页滚动。改为固定控制区和分页、表格数据区独立滚动。Semi UI 2.103.0 本机类型与当前官方文档均支持 SideSheet 字符串 width。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
完成:`pnpm --dir frontend typecheck`、`pnpm --dir frontend build`、`git diff --check` 通过。
|
||||||
|
相关 Playwright 回归 11/11 通过(Alpha 管理、回测、数据目录、QuantFlow、研究流水线、跨页研究导航、特征转模板)。
|
||||||
|
隔离测试服务与浏览器数据验证:1440px/390px 视口抽屉宽度分别 864px/234px;长列表只滚动数据区,分页 y=834 不随内容滚动;body 高度不超过视口。QuantFlow 独立详情支持前进、后退、刷新。
|
||||||
|
独立只读核验 `/root/route_review` complete:未发现路由或草稿恢复的严重阻断。
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
Completion: complete
|
||||||
|
应用选定 Flex 高度链经验,未修改经验库。共享 ResearchList 管理固定控制区、分页及数据视口;ResearchDetail 使用 60% 右侧抽屉。QuantFlow 使用独立 hash 详情路由。保留工作区同时出现的部署相关改动,本次未提交或部署。
|
||||||
@@ -24,7 +24,8 @@ export function contextLabel(context: PageContext): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pageFromHash(hash: string): WorkspacePage {
|
export function pageFromHash(hash: string): WorkspacePage {
|
||||||
const page = hash.replace(/^#/, "");
|
const value = hash.replace(/^#/, "");
|
||||||
|
const page = value.startsWith("quantflow/") ? "quantflow" : value;
|
||||||
return Object.hasOwn(contextLabels, page)
|
return Object.hasOwn(contextLabels, page)
|
||||||
? (page as WorkspacePage)
|
? (page as WorkspacePage)
|
||||||
: "alphas";
|
: "alphas";
|
||||||
|
|||||||
@@ -318,7 +318,8 @@ export function BacktestPage({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="backtest-spacer" />
|
</div>
|
||||||
|
<div className="backtest-toolbar">
|
||||||
<Button
|
<Button
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -438,7 +439,7 @@ export function BacktestPage({
|
|||||||
<SideSheet
|
<SideSheet
|
||||||
title={preview ? "确认回测输入" : "准备候选"}
|
title={preview ? "确认回测输入" : "准备候选"}
|
||||||
visible={editor && drawerVisible}
|
visible={editor && drawerVisible}
|
||||||
width={Math.min(880, window.innerWidth - chatOffset)}
|
width={`min(60vw, calc(100vw - ${chatOffset}px))`}
|
||||||
style={{ right: chatOffset }}
|
style={{ right: chatOffset }}
|
||||||
maskStyle={{ right: chatOffset }}
|
maskStyle={{ right: chatOffset }}
|
||||||
onCancel={() => setEditor(false)}
|
onCancel={() => setEditor(false)}
|
||||||
@@ -730,7 +731,7 @@ export function BacktestPage({
|
|||||||
<SideSheet
|
<SideSheet
|
||||||
title={run?.name || "回测详情"}
|
title={run?.name || "回测详情"}
|
||||||
visible={!!runId && drawerVisible}
|
visible={!!runId && drawerVisible}
|
||||||
width={Math.min(1120, window.innerWidth - chatOffset)}
|
width={`min(60vw, calc(100vw - ${chatOffset}px))`}
|
||||||
style={{ right: chatOffset }}
|
style={{ right: chatOffset }}
|
||||||
maskStyle={{ right: chatOffset }}
|
maskStyle={{ right: chatOffset }}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.backtest-toolbar {
|
.backtest-toolbar {
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -65,8 +66,12 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.backtest-page-view {
|
.backtest-page-view {
|
||||||
height: 100%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.backtest-tool-summary {
|
.backtest-tool-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function AlphaDetail({
|
|||||||
closeOnEsc={!taskPanelOpen}
|
closeOnEsc={!taskPanelOpen}
|
||||||
className="alpha-detail"
|
className="alpha-detail"
|
||||||
visible={Boolean(id) && !suspended}
|
visible={Boolean(id) && !suspended}
|
||||||
width={Math.min(800, window.innerWidth - chatOffset)}
|
width={`min(60vw, calc(100vw - ${chatOffset}px))`}
|
||||||
style={{ right: chatOffset }}
|
style={{ right: chatOffset }}
|
||||||
maskStyle={{ right: chatOffset }}
|
maskStyle={{ right: chatOffset }}
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -514,45 +514,6 @@ export function AlphaPage({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<SavedViews
|
|
||||||
filters={{ ...filters, submission, sort, direction, limit: pageSize }}
|
|
||||||
columns={visibleColumns}
|
|
||||||
onRestore={(view) => {
|
|
||||||
const {
|
|
||||||
submission: savedSubmission,
|
|
||||||
sort: savedSort,
|
|
||||||
direction: savedDirection,
|
|
||||||
limit,
|
|
||||||
offset: _offset,
|
|
||||||
...rest
|
|
||||||
} = view.filters;
|
|
||||||
const values = Object.fromEntries(
|
|
||||||
Object.entries(rest)
|
|
||||||
.filter(([, v]) => v !== null && v !== undefined)
|
|
||||||
.map(([k, v]) => [k, String(v)]),
|
|
||||||
);
|
|
||||||
setFilters(values);
|
|
||||||
setDraft(values);
|
|
||||||
setPage(1);
|
|
||||||
setSelected([]);
|
|
||||||
if (
|
|
||||||
savedSubmission === "SUBMITTED" ||
|
|
||||||
savedSubmission === "UNSUBMITTED"
|
|
||||||
)
|
|
||||||
setSubmission(savedSubmission);
|
|
||||||
setSort(String(savedSort || "date_created"));
|
|
||||||
setDirection(String(savedDirection || "desc"));
|
|
||||||
if (typeof limit === "number") setPageSize(limit);
|
|
||||||
const next = [
|
|
||||||
...new Set([
|
|
||||||
"name",
|
|
||||||
...view.columns.filter((c) => c in columnLabels),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
setVisibleColumns(next);
|
|
||||||
localStorage.setItem("alpha-columns", JSON.stringify(next));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form
|
<form
|
||||||
className="filter-panel"
|
className="filter-panel"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -592,12 +553,6 @@ export function AlphaPage({
|
|||||||
optionList={stateOptions}
|
optionList={stateOptions}
|
||||||
onChange={(v) => updateDraft("research_state", v)}
|
onChange={(v) => updateDraft("research_state", v)}
|
||||||
/>
|
/>
|
||||||
<Button htmlType="submit" theme="solid">
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
<Button type="tertiary" theme="borderless" onClick={clearFilters}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<details className="advanced-filters">
|
<details className="advanced-filters">
|
||||||
<summary>
|
<summary>
|
||||||
@@ -724,7 +679,55 @@ export function AlphaPage({
|
|||||||
数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。
|
数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
<div className="inline-actions filter-actions">
|
||||||
|
<Button htmlType="submit" theme="solid">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button type="tertiary" theme="borderless" onClick={clearFilters}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<SavedViews
|
||||||
|
filters={{ ...filters, submission, sort, direction, limit: pageSize }}
|
||||||
|
columns={visibleColumns}
|
||||||
|
onRestore={(view) => {
|
||||||
|
const {
|
||||||
|
submission: savedSubmission,
|
||||||
|
sort: savedSort,
|
||||||
|
direction: savedDirection,
|
||||||
|
limit,
|
||||||
|
offset: _offset,
|
||||||
|
...rest
|
||||||
|
} = view.filters;
|
||||||
|
const values = Object.fromEntries(
|
||||||
|
Object.entries(rest)
|
||||||
|
.filter(([, v]) => v !== null && v !== undefined)
|
||||||
|
.map(([k, v]) => [k, String(v)]),
|
||||||
|
);
|
||||||
|
setFilters(values);
|
||||||
|
setDraft(values);
|
||||||
|
setPage(1);
|
||||||
|
setSelected([]);
|
||||||
|
if (
|
||||||
|
savedSubmission === "SUBMITTED" ||
|
||||||
|
savedSubmission === "UNSUBMITTED"
|
||||||
|
)
|
||||||
|
setSubmission(savedSubmission);
|
||||||
|
setSort(String(savedSort || "date_created"));
|
||||||
|
setDirection(String(savedDirection || "desc"));
|
||||||
|
if (typeof limit === "number") setPageSize(limit);
|
||||||
|
const next = [
|
||||||
|
...new Set([
|
||||||
|
"name",
|
||||||
|
...view.columns.filter((c) => c in columnLabels),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
setVisibleColumns(next);
|
||||||
|
localStorage.setItem("alpha-columns", JSON.stringify(next));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="table-toolbar">
|
<div className="table-toolbar">
|
||||||
<div>
|
<div>
|
||||||
<strong>
|
<strong>
|
||||||
|
|||||||
@@ -981,24 +981,6 @@ export function DatasetPage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
loading={busy}
|
|
||||||
disabled={!scopeValid}
|
|
||||||
onClick={() => void sync()}
|
|
||||||
>
|
|
||||||
同步目录
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
remember();
|
|
||||||
setHistoryOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
已保存输入 ({drafts.length})
|
|
||||||
</Button>
|
|
||||||
<span className="muted">
|
|
||||||
{formatTime(data.synced_at, account?.timezone)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{scopeError && (
|
{scopeError && (
|
||||||
<Banner
|
<Banner
|
||||||
@@ -1041,9 +1023,27 @@ export function DatasetPage({
|
|||||||
optionList={options(data.categories[browse.category] ?? [])}
|
optionList={options(data.categories[browse.category] ?? [])}
|
||||||
onChange={(v) => editBrowse({ subcategory: String(v ?? "") })}
|
onChange={(v) => editBrowse({ subcategory: String(v ?? "") })}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setBrowse(initialBrowse)}>重置筛选</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="catalog-tools">
|
<div className="catalog-tools">
|
||||||
|
<Button
|
||||||
|
loading={busy}
|
||||||
|
disabled={!scopeValid}
|
||||||
|
onClick={() => void sync()}
|
||||||
|
>
|
||||||
|
同步目录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
remember();
|
||||||
|
setHistoryOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
已保存输入 ({drafts.length})
|
||||||
|
</Button>
|
||||||
|
<span className="muted">
|
||||||
|
{formatTime(data.synced_at, account?.timezone)}
|
||||||
|
</span>
|
||||||
|
<Button onClick={() => setBrowse(initialBrowse)}>重置筛选</Button>
|
||||||
<span aria-live="polite">{summary}</span>
|
<span aria-live="polite">{summary}</span>
|
||||||
{restoreAction}
|
{restoreAction}
|
||||||
{inputAction}
|
{inputAction}
|
||||||
@@ -1075,24 +1075,11 @@ export function DatasetPage({
|
|||||||
{...panelProps}
|
{...panelProps}
|
||||||
className="catalog-sheet catalog-fields"
|
className="catalog-sheet catalog-fields"
|
||||||
visible={fieldsOpen && !suspended && active}
|
visible={fieldsOpen && !suspended && active}
|
||||||
width={viewport < 900 ? viewport : viewport * 0.75}
|
width="60%"
|
||||||
mask={top === "fields"}
|
mask={top === "fields"}
|
||||||
>
|
>
|
||||||
<div className="catalog-layer" ref={fieldsRef}>
|
<div className="catalog-layer" ref={fieldsRef}>
|
||||||
{fieldError && <Banner type="danger" description={fieldError} />}
|
{fieldError && <Banner type="danger" description={fieldError} />}
|
||||||
<div className="catalog-tools">
|
|
||||||
<Button onClick={onChat}>AI 助手</Button>
|
|
||||||
<Button aria-label="关闭字段列表" onClick={close}>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
<span>{summary}</span>
|
|
||||||
{restoreAction}
|
|
||||||
{inputAction}
|
|
||||||
<Button loading={busy} onClick={() => void sync(selected?.id)}>
|
|
||||||
刷新字段
|
|
||||||
</Button>
|
|
||||||
{jobStatus}
|
|
||||||
</div>
|
|
||||||
<div className="catalog-tools">
|
<div className="catalog-tools">
|
||||||
<span>
|
<span>
|
||||||
{scope.region} · {scope.universe} · Delay {scope.delay}
|
{scope.region} · {scope.universe} · Delay {scope.delay}
|
||||||
@@ -1130,9 +1117,22 @@ export function DatasetPage({
|
|||||||
editBrowse({ coverage_min: String(v ?? "") }, true)
|
editBrowse({ coverage_min: String(v ?? "") }, true)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="catalog-tools">
|
||||||
<Button onClick={() => setFieldBrowse(initialBrowse)}>
|
<Button onClick={() => setFieldBrowse(initialBrowse)}>
|
||||||
重置筛选
|
重置筛选
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={onChat}>AI 助手</Button>
|
||||||
|
<Button aria-label="关闭字段列表" onClick={close}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<span>{summary}</span>
|
||||||
|
{restoreAction}
|
||||||
|
{inputAction}
|
||||||
|
<Button loading={busy} onClick={() => void sync(selected?.id)}>
|
||||||
|
刷新字段
|
||||||
|
</Button>
|
||||||
|
{jobStatus}
|
||||||
</div>
|
</div>
|
||||||
<Table<Entry>
|
<Table<Entry>
|
||||||
className="alpha-table catalog-table"
|
className="alpha-table catalog-table"
|
||||||
@@ -1161,11 +1161,7 @@ export function DatasetPage({
|
|||||||
zIndex={1101}
|
zIndex={1101}
|
||||||
className="catalog-sheet catalog-detail"
|
className="catalog-sheet catalog-detail"
|
||||||
visible={!!detail && !suspended && active}
|
visible={!!detail && !suspended && active}
|
||||||
width={
|
width="60%"
|
||||||
detail?.field && viewport >= 900
|
|
||||||
? viewport * 0.3
|
|
||||||
: Math.min(560, viewport)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="catalog-layer" ref={detailRef}>
|
<div className="catalog-layer" ref={detailRef}>
|
||||||
<div className="catalog-tools">
|
<div className="catalog-tools">
|
||||||
@@ -1288,7 +1284,7 @@ export function DatasetPage({
|
|||||||
zIndex={1102}
|
zIndex={1102}
|
||||||
className="catalog-sheet"
|
className="catalog-sheet"
|
||||||
visible={inputOpen && !suspended && active}
|
visible={inputOpen && !suspended && active}
|
||||||
width={Math.min(560, viewport)}
|
width="60%"
|
||||||
>
|
>
|
||||||
<div className="catalog-layer" ref={inputRef}>
|
<div className="catalog-layer" ref={inputRef}>
|
||||||
<div className="catalog-tools">
|
<div className="catalog-tools">
|
||||||
@@ -1349,7 +1345,7 @@ export function DatasetPage({
|
|||||||
zIndex={1103}
|
zIndex={1103}
|
||||||
className="catalog-sheet"
|
className="catalog-sheet"
|
||||||
visible={historyOpen && !suspended && active}
|
visible={historyOpen && !suspended && active}
|
||||||
width={Math.min(560, viewport)}
|
width="60%"
|
||||||
>
|
>
|
||||||
<div className="catalog-layer" ref={historyRef}>
|
<div className="catalog-layer" ref={historyRef}>
|
||||||
<div className="catalog-tools">
|
<div className="catalog-tools">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResearchList, ResearchDetail } from "./ResearchList";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
@@ -41,6 +42,7 @@ export function FeaturesPage({
|
|||||||
onAction: (action: UIAction) => void;
|
onAction: (action: UIAction) => void;
|
||||||
onContext: (context: PageContext) => void;
|
onContext: (context: PageContext) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [draft, setDraft] = useState<Feature>(blank);
|
const [draft, setDraft] = useState<Feature>(blank);
|
||||||
const [asset, setAsset] = useState<FeatureAsset | null>(null);
|
const [asset, setAsset] = useState<FeatureAsset | null>(null);
|
||||||
const [items, setItems] = useState<FeatureAsset[]>([]);
|
const [items, setItems] = useState<FeatureAsset[]>([]);
|
||||||
@@ -95,6 +97,7 @@ export function FeaturesPage({
|
|||||||
);
|
);
|
||||||
}, [active, action]);
|
}, [active, action]);
|
||||||
async function load(item: FeatureAsset) {
|
async function load(item: FeatureAsset) {
|
||||||
|
setDetailOpen(true);
|
||||||
setAsset(item);
|
setAsset(item);
|
||||||
setDraft(item.content);
|
setDraft(item.content);
|
||||||
setVersions(await api(`/research/assets/${item.id}/versions`));
|
setVersions(await api(`/research/assets/${item.id}/versions`));
|
||||||
@@ -127,27 +130,12 @@ export function FeaturesPage({
|
|||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="research-workspace">
|
<section className="research-workspace research-list-page">
|
||||||
<div className="section-toolbar">
|
{error && !detailOpen && <Banner type="danger" description={error} />}
|
||||||
<div>
|
<ResearchList
|
||||||
<h2>特征工程</h2>
|
resetKey={JSON.stringify([q, page])}
|
||||||
<p className="muted">
|
label="特征方案"
|
||||||
围绕固定字段记录处理步骤与经济假设,再进入模板、候选和回测闭环。
|
filters={
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setAsset(null);
|
|
||||||
setDraft(blank());
|
|
||||||
setVersions([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
新建特征方案
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{error && <Banner type="danger" description={error} />}
|
|
||||||
<div className="research-layout">
|
|
||||||
<aside className="research-library">
|
|
||||||
<Input
|
<Input
|
||||||
aria-label="搜索特征方案"
|
aria-label="搜索特征方案"
|
||||||
placeholder="搜索方案"
|
placeholder="搜索方案"
|
||||||
@@ -157,16 +145,24 @@ export function FeaturesPage({
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{items.map((item) => (
|
}
|
||||||
|
actions={
|
||||||
<Button
|
<Button
|
||||||
key={item.id}
|
theme="solid"
|
||||||
block
|
onClick={() => {
|
||||||
theme={asset?.id === item.id ? "light" : "borderless"}
|
setAsset(null);
|
||||||
onClick={() => void run("load", () => load(item))}
|
setDraft(blank());
|
||||||
|
setVersions([]);
|
||||||
|
setDetailOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.name} · v{item.version}
|
新建特征方案
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
}
|
||||||
|
pagination={
|
||||||
|
<>
|
||||||
|
<span>共 {total} 个方案</span>
|
||||||
|
{total > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
@@ -174,7 +170,51 @@ export function FeaturesPage({
|
|||||||
total={total}
|
total={total}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
/>
|
/>
|
||||||
</aside>
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</ResearchList>
|
||||||
|
<ResearchDetail
|
||||||
|
title={asset ? asset.name : "新建特征方案"}
|
||||||
|
visible={active && detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
|
>
|
||||||
|
{error && <Banner type="danger" description={error} />}
|
||||||
<div className="research-main">
|
<div className="research-main">
|
||||||
<div className="inline-actions">
|
<div className="inline-actions">
|
||||||
{asset && (
|
{asset && (
|
||||||
@@ -380,7 +420,7 @@ export function FeaturesPage({
|
|||||||
保存步骤不会计算原始时间序列。转换后的模板保留此方案的完整版本,回测前仍需展开校验并确认候选。
|
保存步骤不会计算原始时间序列。转换后的模板保留此方案的完整版本,回测前仍需展开校验并确认候选。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResearchDetail>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResearchList, ResearchDetail } from "./ResearchList";
|
||||||
import { ResearchSelect } from "./ResearchSelect";
|
import { ResearchSelect } from "./ResearchSelect";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -61,29 +62,12 @@ export function OperatorsPage({ active }: { active: boolean }) {
|
|||||||
if (active) void task(load);
|
if (active) void task(load);
|
||||||
}, [active, query, category, favorite, offset]);
|
}, [active, query, category, favorite, offset]);
|
||||||
return (
|
return (
|
||||||
<div className="research-workspace">
|
<div className="research-workspace research-list-page">
|
||||||
<header className="research-page-heading">
|
{error && !selected && <Banner type="danger" description={error} />}
|
||||||
<div>
|
<ResearchList
|
||||||
<h2>算子库</h2>
|
resetKey={JSON.stringify([query, category, favorite, offset])}
|
||||||
<p>
|
label="算子库"
|
||||||
平台定义只读,本地收藏和研究备注独立保存。同步时间:
|
filters={
|
||||||
{formatTime(result.fetched_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
theme="solid"
|
|
||||||
loading={busy}
|
|
||||||
onClick={() =>
|
|
||||||
void task(async () => {
|
|
||||||
await post("/catalog/operators/refresh");
|
|
||||||
await load();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
同步算子目录
|
|
||||||
</Button>
|
|
||||||
</header>
|
|
||||||
{error && <Banner type="danger" description={error} />}
|
|
||||||
<div className="research-toolbar">
|
<div className="research-toolbar">
|
||||||
<Input
|
<Input
|
||||||
aria-label="搜索算子"
|
aria-label="搜索算子"
|
||||||
@@ -116,39 +100,27 @@ export function OperatorsPage({ active }: { active: boolean }) {
|
|||||||
只看收藏
|
只看收藏
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div className="research-operator-layout">
|
}
|
||||||
<section className="research-card">
|
actions={
|
||||||
<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
|
<Button
|
||||||
theme="borderless"
|
theme="solid"
|
||||||
onClick={() => setSelected(item)}
|
loading={busy}
|
||||||
|
onClick={() =>
|
||||||
|
void task(async () => {
|
||||||
|
await post("/catalog/operators/refresh");
|
||||||
|
await load();
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{item.local.favorite ? "已收藏 · " : ""}
|
同步算子目录
|
||||||
{item.name}
|
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
<span className="muted">
|
||||||
<td>{item.category}</td>
|
同步时间:{formatTime(result.fetched_at)}
|
||||||
<td>
|
</span>
|
||||||
<code>{item.definition}</code>
|
</>
|
||||||
</td>
|
}
|
||||||
</tr>
|
pagination={
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{!result.items.length && (
|
|
||||||
<p>暂无算子。连接平台并同步目录,或调整筛选条件。</p>
|
|
||||||
)}
|
|
||||||
<div className="research-toolbar">
|
<div className="research-toolbar">
|
||||||
<span>共 {result.total} 个</span>
|
<span>共 {result.total} 个</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -164,7 +136,43 @@ export function OperatorsPage({ active }: { active: boolean }) {
|
|||||||
下一页
|
下一页
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</ResearchList>
|
||||||
|
<ResearchDetail
|
||||||
|
title={selected?.name || "算子详情"}
|
||||||
|
visible={active && !!selected}
|
||||||
|
onClose={() => setSelected(null)}
|
||||||
|
>
|
||||||
|
{error && <Banner type="danger" description={error} />}
|
||||||
{selected && (
|
{selected && (
|
||||||
<section className="research-card">
|
<section className="research-card">
|
||||||
<h3>{selected.name}</h3>
|
<h3>{selected.name}</h3>
|
||||||
@@ -225,7 +233,7 @@ export function OperatorsPage({ active }: { active: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ResearchDetail>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResearchList, ResearchDetail } from "./ResearchList";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Banner, Button, Pagination } from "@douyinfe/semi-ui-19";
|
import { Banner, Button, Pagination } from "@douyinfe/semi-ui-19";
|
||||||
import { api, formatTime } from "../api";
|
import { api, formatTime } from "../api";
|
||||||
@@ -23,7 +24,8 @@ export function PipelinePage({
|
|||||||
localStorage.getItem("research-selected-flow"),
|
localStorage.getItem("research-selected-flow"),
|
||||||
);
|
);
|
||||||
const [run, setRun] = useState<FlowRun | null>(null);
|
const [run, setRun] = useState<FlowRun | null>(null);
|
||||||
const [creating, setCreating] = useState(!selected);
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [revision, setRevision] = useState(0);
|
const [revision, setRevision] = useState(0);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,38 +61,33 @@ export function PipelinePage({
|
|||||||
};
|
};
|
||||||
}, [active, page, selected, revision]);
|
}, [active, page, selected, revision]);
|
||||||
function select(id: string) {
|
function select(id: string) {
|
||||||
|
setRun(null);
|
||||||
|
setDetailOpen(true);
|
||||||
setSelected(id);
|
setSelected(id);
|
||||||
localStorage.setItem("research-selected-flow", id);
|
localStorage.setItem("research-selected-flow", id);
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="research-workspace">
|
<section className="research-workspace research-list-page">
|
||||||
<div className="section-toolbar">
|
{error && !detailOpen && <Banner type="danger" description={error} />}
|
||||||
<div>
|
<ResearchList
|
||||||
<h2>研究流水线</h2>
|
resetKey={JSON.stringify([page])}
|
||||||
<p className="muted">
|
label="研究流水线"
|
||||||
生成 → 校验与设参 → 回测 → 评估决策 → 增强 → 重新展开
|
actions={
|
||||||
</p>
|
<Button
|
||||||
</div>
|
theme="solid"
|
||||||
<Button onClick={() => setCreating(true)}>新建自动研究</Button>
|
onClick={() => {
|
||||||
</div>
|
setCreating(true);
|
||||||
{error && <Banner type="danger" description={error} />}
|
setDetailOpen(true);
|
||||||
<div className="research-layout">
|
}}
|
||||||
<aside className="research-library">
|
|
||||||
<h3>研究运行</h3>
|
|
||||||
{items.map((item) => (
|
|
||||||
<button
|
|
||||||
className={`research-library-item ${selected === item.id ? "selected" : ""}`}
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => select(item.id)}
|
|
||||||
>
|
>
|
||||||
<strong>{item.name}</strong>
|
新建自动研究
|
||||||
<span>
|
</Button>
|
||||||
{flowStatus[item.status]} · 第 {item.round} 轮
|
}
|
||||||
</span>
|
pagination={
|
||||||
<span>{formatTime(item.created_at)}</span>
|
<>
|
||||||
</button>
|
<span>共 {total} 次运行</span>
|
||||||
))}
|
{total > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
total={total}
|
total={total}
|
||||||
@@ -98,7 +95,44 @@ export function PipelinePage({
|
|||||||
pageSize={25}
|
pageSize={25}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
/>
|
/>
|
||||||
</aside>
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</ResearchList>
|
||||||
|
<ResearchDetail
|
||||||
|
title={creating ? "新建自动研究" : "研究运行详情"}
|
||||||
|
visible={active && detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
|
>
|
||||||
|
{error && <Banner type="danger" description={error} />}
|
||||||
<div className="research-main">
|
<div className="research-main">
|
||||||
{creating ? (
|
{creating ? (
|
||||||
<FlowLaunchForm
|
<FlowLaunchForm
|
||||||
@@ -119,7 +153,7 @@ export function PipelinePage({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResearchDetail>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResearchList } from "./ResearchList";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Toast,
|
Toast,
|
||||||
} from "@douyinfe/semi-ui-19";
|
} from "@douyinfe/semi-ui-19";
|
||||||
import { api, post } from "../api";
|
import { api, post, formatTime } from "../api";
|
||||||
import type { PageContext, UIAction } from "../ai/types";
|
import type { PageContext, UIAction } from "../ai/types";
|
||||||
import { FlowCanvas, type NodeType } from "./FlowCanvas";
|
import { FlowCanvas, type NodeType } from "./FlowCanvas";
|
||||||
import { FlowLaunchForm } from "./FlowLaunchForm";
|
import { FlowLaunchForm } from "./FlowLaunchForm";
|
||||||
@@ -50,6 +51,18 @@ export function QuantFlowPage({
|
|||||||
onContext: (c: PageContext) => void;
|
onContext: (c: PageContext) => void;
|
||||||
onAction: (a: UIAction) => void;
|
onAction: (a: UIAction) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [route, setRoute] = useState(() => location.hash);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const detail = /^#quantflow\/(new|workflows\/[^/]+|runs\/[^/]+)$/.test(route);
|
||||||
|
useEffect(() => {
|
||||||
|
const changed = () => setRoute(location.hash);
|
||||||
|
window.addEventListener("hashchange", changed);
|
||||||
|
return () => window.removeEventListener("hashchange", changed);
|
||||||
|
}, []);
|
||||||
|
function navigate(path: string) {
|
||||||
|
location.hash = `quantflow${path}`;
|
||||||
|
setRoute(`#quantflow${path}`);
|
||||||
|
}
|
||||||
const [assets, setAssets] = useState<WorkflowAsset[]>([]),
|
const [assets, setAssets] = useState<WorkflowAsset[]>([]),
|
||||||
[total, setTotal] = useState(0),
|
[total, setTotal] = useState(0),
|
||||||
[page, setPage] = useState(1);
|
[page, setPage] = useState(1);
|
||||||
@@ -106,7 +119,7 @@ export function QuantFlowPage({
|
|||||||
const c = new AbortController();
|
const c = new AbortController();
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api<{ items: WorkflowAsset[]; total: number }>(
|
api<{ items: WorkflowAsset[]; total: number }>(
|
||||||
`/research/assets?kind=workflow&offset=${(page - 1) * 25}`,
|
`/research/assets?kind=workflow&offset=${(page - 1) * 25}&q=${encodeURIComponent(query)}`,
|
||||||
{ signal: c.signal },
|
{ signal: c.signal },
|
||||||
),
|
),
|
||||||
api<{ items: NodeType[] }>("/research/flows/nodes", { signal: c.signal }),
|
api<{ items: NodeType[] }>("/research/flows/nodes", { signal: c.signal }),
|
||||||
@@ -127,7 +140,7 @@ export function QuantFlowPage({
|
|||||||
if (!c.signal.aborted) setError(e.message);
|
if (!c.signal.aborted) setError(e.message);
|
||||||
});
|
});
|
||||||
return () => c.abort();
|
return () => c.abort();
|
||||||
}, [active, page, revision]);
|
}, [active, page, revision, query]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active || mode !== "runs") return;
|
if (!active || mode !== "runs") return;
|
||||||
const c = new AbortController();
|
const c = new AbortController();
|
||||||
@@ -156,6 +169,41 @@ export function QuantFlowPage({
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [active, mode, runId, runPage, revision]);
|
}, [active, mode, runId, runPage, revision]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
const runMatch = route.match(/^#quantflow\/runs\/([^/]+)$/);
|
||||||
|
const workflowMatch = route.match(/^#quantflow\/workflows\/([^/]+)$/);
|
||||||
|
if (runMatch) {
|
||||||
|
setMode("runs");
|
||||||
|
if (runId !== runMatch[1]) {
|
||||||
|
setRun(null);
|
||||||
|
setRunId(runMatch[1]);
|
||||||
|
}
|
||||||
|
} else if (route === "#quantflow/runs") {
|
||||||
|
setMode("runs");
|
||||||
|
} else {
|
||||||
|
setMode("edit");
|
||||||
|
if (route === "#quantflow/new" && asset) {
|
||||||
|
setAsset(null);
|
||||||
|
setVersions([]);
|
||||||
|
change(readDraft("new", empty));
|
||||||
|
}
|
||||||
|
if (workflowMatch && asset?.id !== workflowMatch[1]) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void api<WorkflowAsset>(
|
||||||
|
`/research/assets/${encodeURIComponent(workflowMatch[1])}`,
|
||||||
|
{ signal: controller.signal },
|
||||||
|
)
|
||||||
|
.then((item) => {
|
||||||
|
if (!controller.signal.aborted) void open(item);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (!controller.signal.aborted) setError(e.message);
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [active, route]);
|
||||||
function change(value: Workflow) {
|
function change(value: Workflow) {
|
||||||
setGraph(value);
|
setGraph(value);
|
||||||
setValid(false);
|
setValid(false);
|
||||||
@@ -179,6 +227,7 @@ export function QuantFlowPage({
|
|||||||
/* Keep the server version when the local draft is unavailable. */
|
/* Keep the server version when the local draft is unavailable. */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
navigate(`/workflows/${item.id}`);
|
||||||
setAsset(baseline);
|
setAsset(baseline);
|
||||||
change(version ? loaded.content : readDraft(loaded.id, loaded.content));
|
change(version ? loaded.content : readDraft(loaded.id, loaded.content));
|
||||||
setSelected(loaded.content.nodes[0]?.id || null);
|
setSelected(loaded.content.nodes[0]?.id || null);
|
||||||
@@ -208,6 +257,8 @@ export function QuantFlowPage({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
history.replaceState(null, "", `#quantflow/workflows/${result.id}`);
|
||||||
|
setRoute(`#quantflow/workflows/${result.id}`);
|
||||||
setAsset(result);
|
setAsset(result);
|
||||||
setGraph(result.content);
|
setGraph(result.content);
|
||||||
setValid(true);
|
setValid(true);
|
||||||
@@ -288,21 +339,48 @@ export function QuantFlowPage({
|
|||||||
change({ ...graph, edges: [...graph.edges, edge] });
|
change({ ...graph, edges: [...graph.edges, edge] });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="research-workspace quantflow-workspace">
|
<section className="research-workspace research-list-page quantflow-workspace">
|
||||||
<div className="section-toolbar">
|
{error && <Banner type="danger" description={error} />}
|
||||||
<div>
|
{!detail ? (
|
||||||
<h2>QuantFlow</h2>
|
<ResearchList
|
||||||
<p className="muted">组合原生研究节点,复用回测、评估与有限预算。</p>
|
resetKey={JSON.stringify([mode, query, page, runPage])}
|
||||||
</div>
|
label="QuantFlow"
|
||||||
<div className="inline-actions">
|
filters={
|
||||||
<Button onClick={() => setMode("edit")}>编辑画布</Button>
|
<>
|
||||||
<Button onClick={() => setMode("runs")}>运行记录</Button>
|
<select
|
||||||
|
aria-label="QuantFlow 列表"
|
||||||
|
value={mode === "runs" ? "runs" : "workflows"}
|
||||||
|
onChange={(e) => {
|
||||||
|
setMode(e.target.value === "runs" ? "runs" : "edit");
|
||||||
|
navigate(e.target.value === "runs" ? "/runs" : "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="workflows">流程库</option>
|
||||||
|
<option value="runs">运行记录</option>
|
||||||
|
</select>
|
||||||
|
{mode !== "runs" && (
|
||||||
|
<Input
|
||||||
|
aria-label="搜索流程"
|
||||||
|
placeholder="搜索流程名称"
|
||||||
|
value={query}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuery(value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
theme="solid"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAsset(null);
|
setAsset(null);
|
||||||
setVersions([]);
|
setVersions([]);
|
||||||
change(readDraft("new", empty));
|
change(readDraft("new", empty));
|
||||||
setMode("edit");
|
setMode("edit");
|
||||||
|
navigate("/new");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
新建流程
|
新建流程
|
||||||
@@ -310,11 +388,14 @@ export function QuantFlowPage({
|
|||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const recipe = await api<Workflow>("/research/flows/recipe");
|
const recipe = await api<Workflow>(
|
||||||
|
"/research/flows/recipe",
|
||||||
|
);
|
||||||
setAsset(null);
|
setAsset(null);
|
||||||
setVersions([]);
|
setVersions([]);
|
||||||
change({ ...recipe, name: "固定研究配方副本" });
|
change({ ...recipe, name: "固定研究配方副本" });
|
||||||
setMode("edit");
|
setMode("edit");
|
||||||
|
navigate("/new");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError((e as Error).message);
|
setError((e as Error).message);
|
||||||
}
|
}
|
||||||
@@ -322,62 +403,106 @@ export function QuantFlowPage({
|
|||||||
>
|
>
|
||||||
复制固定配方
|
复制固定配方
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</>
|
||||||
</div>
|
}
|
||||||
{error && <Banner type="danger" description={error} />}
|
pagination={
|
||||||
<div className="research-layout">
|
<>
|
||||||
<aside className="research-library">
|
<span>共 {mode === "runs" ? runTotal : total} 条</span>
|
||||||
<h3>{mode === "runs" ? "画布运行" : "流程库"}</h3>
|
{(mode === "runs" ? runTotal : total) > 0 && (
|
||||||
|
<Pagination
|
||||||
|
size="small"
|
||||||
|
total={mode === "runs" ? runTotal : total}
|
||||||
|
currentPage={mode === "runs" ? runPage : page}
|
||||||
|
pageSize={25}
|
||||||
|
onPageChange={mode === "runs" ? setRunPage : setPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{mode === "runs" ? (
|
{mode === "runs" ? (
|
||||||
<>
|
<>
|
||||||
|
<table className="research-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>运行名称</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>轮次</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{runs.map((r) => (
|
{runs.map((r) => (
|
||||||
<button
|
<tr key={r.id}>
|
||||||
className={`research-library-item ${runId === r.id ? "selected" : ""}`}
|
<td>
|
||||||
key={r.id}
|
<Button
|
||||||
|
theme="borderless"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRunId(r.id);
|
setRunId(r.id);
|
||||||
setRun(null);
|
setRun(null);
|
||||||
localStorage.setItem("quantflow-selected-run", r.id);
|
localStorage.setItem(
|
||||||
|
"quantflow-selected-run",
|
||||||
|
r.id,
|
||||||
|
);
|
||||||
|
navigate(`/runs/${r.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{r.name}</strong>
|
{r.name}
|
||||||
<span>
|
</Button>
|
||||||
{flowStatus[r.status]} · 第 {r.round} 轮
|
</td>
|
||||||
</span>
|
<td>{flowStatus[r.status]}</td>
|
||||||
</button>
|
<td>{r.round}</td>
|
||||||
|
<td>{formatTime(r.created_at)}</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
<Pagination
|
</tbody>
|
||||||
size="small"
|
</table>
|
||||||
total={runTotal}
|
{!runs.length && (
|
||||||
currentPage={runPage}
|
<p className="research-list-empty">暂无流程运行记录。</p>
|
||||||
pageSize={25}
|
)}
|
||||||
onPageChange={setRunPage}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<table className="research-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>流程名称</th>
|
||||||
|
<th>版本</th>
|
||||||
|
<th>节点数量</th>
|
||||||
|
<th>连接数量</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{assets.map((a) => (
|
{assets.map((a) => (
|
||||||
<button
|
<tr key={a.id}>
|
||||||
className={`research-library-item ${asset?.id === a.id ? "selected" : ""}`}
|
<td>
|
||||||
key={a.id}
|
<Button theme="borderless" onClick={() => void open(a)}>
|
||||||
onClick={() => void open(a)}
|
{a.name}
|
||||||
>
|
</Button>
|
||||||
<strong>{a.name}</strong>
|
</td>
|
||||||
<span>
|
<td>v{a.version}</td>
|
||||||
v{a.version} · {a.content.nodes.length} 个节点
|
<td>{a.content.nodes.length}</td>
|
||||||
</span>
|
<td>{a.content.edges.length}</td>
|
||||||
</button>
|
</tr>
|
||||||
))}
|
))}
|
||||||
<Pagination
|
</tbody>
|
||||||
size="small"
|
</table>
|
||||||
total={total}
|
{!assets.length && (
|
||||||
currentPage={page}
|
<p className="research-list-empty">
|
||||||
pageSize={25}
|
暂无流程,请新建流程或复制固定配方。
|
||||||
onPageChange={setPage}
|
</p>
|
||||||
/>
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</ResearchList>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="inline-actions quantflow-detail-actions">
|
||||||
|
<Button onClick={() => navigate(mode === "runs" ? "/runs" : "")}>
|
||||||
|
返回列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="quantflow-detail-body">
|
||||||
<div className="research-main">
|
<div className="research-main">
|
||||||
{mode === "runs" ? (
|
{mode === "runs" ? (
|
||||||
run ? (
|
run ? (
|
||||||
@@ -413,7 +538,9 @@ export function QuantFlowPage({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="research-card">选择一条运行查看节点状态和产物。</p>
|
<p className="research-card">
|
||||||
|
选择一条运行查看节点状态和产物。
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
) : mode === "launch" && asset ? (
|
) : mode === "launch" && asset ? (
|
||||||
<>
|
<>
|
||||||
@@ -425,6 +552,7 @@ export function QuantFlowPage({
|
|||||||
setRunId(r.id);
|
setRunId(r.id);
|
||||||
localStorage.setItem("quantflow-selected-run", r.id);
|
localStorage.setItem("quantflow-selected-run", r.id);
|
||||||
setMode("runs");
|
setMode("runs");
|
||||||
|
navigate(`/runs/${r.id}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -468,7 +596,10 @@ export function QuantFlowPage({
|
|||||||
保存流程版本
|
保存流程版本
|
||||||
</Button>
|
</Button>
|
||||||
{asset && (
|
{asset && (
|
||||||
<Button disabled={busy} onClick={() => void save(true)}>
|
<Button
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => void save(true)}
|
||||||
|
>
|
||||||
另存流程
|
另存流程
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -548,9 +679,15 @@ export function QuantFlowPage({
|
|||||||
{graph.edges.map((e, i) => (
|
{graph.edges.map((e, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<span>
|
<span>
|
||||||
{graph.nodes.find((n) => n.id === e.source)?.label}{" "}
|
{
|
||||||
|
graph.nodes.find((n) => n.id === e.source)
|
||||||
|
?.label
|
||||||
|
}{" "}
|
||||||
→{" "}
|
→{" "}
|
||||||
{graph.nodes.find((n) => n.id === e.target)?.label}{" "}
|
{
|
||||||
|
graph.nodes.find((n) => n.id === e.target)
|
||||||
|
?.label
|
||||||
|
}{" "}
|
||||||
{e.branch && `· ${e.branch}`}
|
{e.branch && `· ${e.branch}`}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -559,7 +696,9 @@ export function QuantFlowPage({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
change({
|
change({
|
||||||
...graph,
|
...graph,
|
||||||
edges: graph.edges.filter((_, j) => j !== i),
|
edges: graph.edges.filter(
|
||||||
|
(_, j) => j !== i,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -585,7 +724,10 @@ export function QuantFlowPage({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<Button disabled={graph.nodes.length >= 50} onClick={addNode}>
|
<Button
|
||||||
|
disabled={graph.nodes.length >= 50}
|
||||||
|
onClick={addNode}
|
||||||
|
>
|
||||||
添加节点
|
添加节点
|
||||||
</Button>
|
</Button>
|
||||||
{node && (
|
{node && (
|
||||||
@@ -607,7 +749,9 @@ export function QuantFlowPage({
|
|||||||
aria-label="节点研究说明"
|
aria-label="节点研究说明"
|
||||||
value={String(node.config.prompt || "")}
|
value={String(node.config.prompt || "")}
|
||||||
onChange={(prompt) =>
|
onChange={(prompt) =>
|
||||||
updateNode({ config: { ...node.config, prompt } })
|
updateNode({
|
||||||
|
config: { ...node.config, prompt },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -683,12 +827,18 @@ export function QuantFlowPage({
|
|||||||
生成方法
|
生成方法
|
||||||
<select
|
<select
|
||||||
aria-label="变体方法"
|
aria-label="变体方法"
|
||||||
value={String(node.config.method || "structure")}
|
value={String(
|
||||||
|
node.config.method || "structure",
|
||||||
|
)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateNode({ config: { method: e.target.value } })
|
updateNode({
|
||||||
|
config: { method: e.target.value },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="structure">结构与字段变体</option>
|
<option value="structure">
|
||||||
|
结构与字段变体
|
||||||
|
</option>
|
||||||
<option value="settings">市场与设置变体</option>
|
<option value="settings">市场与设置变体</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -701,7 +851,9 @@ export function QuantFlowPage({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={(
|
checked={(
|
||||||
(node.config.verdicts as string[]) || ["pass"]
|
(node.config.verdicts as string[]) || [
|
||||||
|
"pass",
|
||||||
|
]
|
||||||
).includes(v)}
|
).includes(v)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const values = (node.config
|
const values = (node.config
|
||||||
@@ -753,7 +905,8 @@ export function QuantFlowPage({
|
|||||||
),
|
),
|
||||||
edges: graph.edges.filter(
|
edges: graph.edges.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.source !== node.id && e.target !== node.id,
|
e.source !== node.id &&
|
||||||
|
e.target !== node.id,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
setSelected("input");
|
setSelected("input");
|
||||||
@@ -770,6 +923,8 @@ export function QuantFlowPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
|
import { SideSheet } from "@douyinfe/semi-ui-19";
|
||||||
|
|
||||||
|
/** Parent supplies a bounded flex area; only the data viewport owns scrolling. */
|
||||||
|
export function ResearchList({
|
||||||
|
label,
|
||||||
|
resetKey,
|
||||||
|
filters,
|
||||||
|
actions,
|
||||||
|
pagination,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
resetKey: string;
|
||||||
|
filters?: ReactNode;
|
||||||
|
actions: ReactNode;
|
||||||
|
pagination: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const viewport = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
viewport.current?.scrollTo({ top: 0, left: 0 });
|
||||||
|
}, [resetKey]);
|
||||||
|
return (
|
||||||
|
<section className="research-list" aria-label={label}>
|
||||||
|
{filters && <div className="research-list-filters">{filters}</div>}
|
||||||
|
<div className="research-list-actions">{actions}</div>
|
||||||
|
<div
|
||||||
|
ref={viewport}
|
||||||
|
className="research-list-scroll"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${label}数据区`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="table-pagination">{pagination}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep drafts mounted while closed; inactive workspace pages must hide portals. */
|
||||||
|
export function ResearchDetail({
|
||||||
|
title,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SideSheet
|
||||||
|
title={title}
|
||||||
|
className="research-detail"
|
||||||
|
placement="right"
|
||||||
|
width="60%"
|
||||||
|
visible={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
closeOnEsc
|
||||||
|
keepDOM
|
||||||
|
motion={false}
|
||||||
|
>
|
||||||
|
<div className="research-detail-content">{children}</div>
|
||||||
|
</SideSheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResearchList, ResearchDetail } from "./ResearchList";
|
||||||
import { ResearchSelect } from "./ResearchSelect";
|
import { ResearchSelect } from "./ResearchSelect";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +44,11 @@ export function ResearchWorkspace({
|
|||||||
onAction: (action: UIAction) => void;
|
onAction: (action: UIAction) => void;
|
||||||
onContext: (context: PageContext) => void;
|
onContext: (context: PageContext) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
const [detailMode, setDetailMode] = useState<"editor" | "experiment">(
|
||||||
|
"editor",
|
||||||
|
);
|
||||||
|
const [listMode, setListMode] = useState("assets");
|
||||||
const processedAction = useRef<number | null>(null);
|
const processedAction = useRef<number | null>(null);
|
||||||
const [assets, setAssets] = useState<Asset[]>([]);
|
const [assets, setAssets] = useState<Asset[]>([]);
|
||||||
const [inputs, setInputs] = useState<InputSnapshot[]>([]);
|
const [inputs, setInputs] = useState<InputSnapshot[]>([]);
|
||||||
@@ -119,6 +125,14 @@ export function ResearchWorkspace({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active || !action || processedAction.current === action.nonce) return;
|
if (!active || !action || processedAction.current === action.nonce) return;
|
||||||
processedAction.current = action.nonce;
|
processedAction.current = action.nonce;
|
||||||
|
if (
|
||||||
|
["open_experiment", "open_template", "open_variant"].includes(action.type)
|
||||||
|
) {
|
||||||
|
setDetailOpen(true);
|
||||||
|
setDetailMode(
|
||||||
|
action.type === "open_experiment" ? "experiment" : "editor",
|
||||||
|
);
|
||||||
|
}
|
||||||
if (action.type === "open_experiment")
|
if (action.type === "open_experiment")
|
||||||
void task("读取实验", async () =>
|
void task("读取实验", async () =>
|
||||||
setExperiment(
|
setExperiment(
|
||||||
@@ -244,35 +258,23 @@ export function ResearchWorkspace({
|
|||||||
)
|
)
|
||||||
.toString();
|
.toString();
|
||||||
return (
|
return (
|
||||||
<div className="research-workspace">
|
<div className="research-workspace research-list-page">
|
||||||
<header className="research-page-heading">
|
{error && !detailOpen && <Banner type="danger" description={error} />}
|
||||||
<div>
|
<ResearchList
|
||||||
<h2>{page === "templates" ? "模板工坊" : "Alpha 变体"}</h2>
|
resetKey={JSON.stringify([listMode, search, assetPage, historyPage])}
|
||||||
<p>
|
label={page === "templates" ? "模板工坊" : "Alpha 变体"}
|
||||||
{page === "templates"
|
filters={
|
||||||
? "从固定输入生成候选,保存每一次模板版本与研究依据。"
|
<>
|
||||||
: "围绕已有 Alpha 研究结构、字段与市场设置的变化。"}
|
<ResearchSelect
|
||||||
</p>
|
label="研究列表"
|
||||||
</div>
|
value={listMode}
|
||||||
<Button loading={!!busy} onClick={() => void task("刷新", refresh)}>
|
optionList={[
|
||||||
刷新素材
|
{ value: "assets", label: "模板库" },
|
||||||
</Button>
|
{ value: "history", label: "生成记录" },
|
||||||
</header>
|
]}
|
||||||
{error && <Banner type="danger" description={error} />}
|
onChange={(value) => setListMode(String(value))}
|
||||||
<div className="research-layout">
|
/>
|
||||||
<aside className="research-library">
|
{listMode === "assets" && (
|
||||||
<div className="research-section-heading">
|
|
||||||
<h3>模板库</h3>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setAsset(null);
|
|
||||||
setTemplate(blankTemplate());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
aria-label="搜索模板"
|
aria-label="搜索模板"
|
||||||
placeholder="搜索名称"
|
placeholder="搜索名称"
|
||||||
@@ -282,75 +284,174 @@ export function ResearchWorkspace({
|
|||||||
setAssetPage(0);
|
setAssetPage(0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme="solid"
|
||||||
|
onClick={() => {
|
||||||
|
setAsset(null);
|
||||||
|
setTemplate(blankTemplate());
|
||||||
|
setExperiment(null);
|
||||||
|
setDetailMode("editor");
|
||||||
|
setDetailOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
<Button loading={!!busy} onClick={() => void task("刷新", refresh)}>
|
||||||
|
刷新素材
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
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" ? (
|
||||||
|
<>
|
||||||
|
<table className="research-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>模板名称</th>
|
||||||
|
<th>版本</th>
|
||||||
|
<th>表达式</th>
|
||||||
|
<th>分类</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{assets.map((item) => (
|
{assets.map((item) => (
|
||||||
<button
|
<tr key={item.id}>
|
||||||
className={`research-library-item ${asset?.id === item.id ? "selected" : ""}`}
|
<td>
|
||||||
key={item.id}
|
<Button
|
||||||
|
theme="borderless"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void task("读取模板", async () => {
|
void task("读取模板", async () => {
|
||||||
const next = await api<Asset>(`/research/assets/${item.id}`);
|
const next = await api<Asset>(
|
||||||
|
`/research/assets/${item.id}`,
|
||||||
|
);
|
||||||
setAsset(next);
|
setAsset(next);
|
||||||
setTemplate(next.content);
|
setTemplate(next.content);
|
||||||
|
setExperiment(null);
|
||||||
|
setDetailMode("editor");
|
||||||
|
setDetailOpen(true);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<strong>{item.name}</strong>
|
{item.name}
|
||||||
<span>v{item.version}</span>
|
</Button>
|
||||||
</button>
|
</td>
|
||||||
|
<td>v{item.version}</td>
|
||||||
|
<td>
|
||||||
|
<code>{item.content.expression}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{item.content.category === "fragment" ? "片段" : "模板"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!assets.length && <p>还没有模板。新建或导入一个模板开始研究。</p>}
|
</tbody>
|
||||||
<div className="research-toolbar">
|
</table>
|
||||||
<small>共 {assetTotal} 个模板</small>
|
{!assets.length && (
|
||||||
<Button
|
<p className="research-list-empty">
|
||||||
size="small"
|
还没有模板。新建或导入一个模板开始研究。
|
||||||
disabled={!assetPage}
|
</p>
|
||||||
onClick={() => setAssetPage(assetPage - 1)}
|
)}
|
||||||
>
|
</>
|
||||||
上一页模板
|
) : (
|
||||||
</Button>
|
<>
|
||||||
<Button
|
<table className="research-table">
|
||||||
size="small"
|
<thead>
|
||||||
disabled={(assetPage + 1) * 25 >= assetTotal}
|
<tr>
|
||||||
onClick={() => setAssetPage(assetPage + 1)}
|
<th>研究名称</th>
|
||||||
>
|
<th>候选数量</th>
|
||||||
下一页模板
|
<th>创建时间</th>
|
||||||
</Button>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
<h3>生成记录</h3>
|
<tbody>
|
||||||
{history.map((item) => (
|
{history.map((item) => (
|
||||||
<button
|
<tr key={item.id}>
|
||||||
className="research-library-item"
|
<td>
|
||||||
key={item.id}
|
<Button
|
||||||
|
theme="borderless"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void task("读取实验", async () =>
|
void task("读取实验", async () => {
|
||||||
setExperiment(await api(`/research/experiments/${item.id}`)),
|
setExperiment(
|
||||||
)
|
await api(`/research/experiments/${item.id}`),
|
||||||
|
);
|
||||||
|
setDetailMode("experiment");
|
||||||
|
setDetailOpen(true);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<strong>{item.name}</strong>
|
{item.name}
|
||||||
<span>
|
</Button>
|
||||||
{item.total} 个候选 · {formatTime(item.created_at)}
|
</td>
|
||||||
</span>
|
<td>{item.total}</td>
|
||||||
</button>
|
<td>{formatTime(item.created_at)}</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
<div className="research-toolbar">
|
</tbody>
|
||||||
<small>共 {historyTotal} 次生成</small>
|
</table>
|
||||||
<Button
|
{!history.length && (
|
||||||
size="small"
|
<p className="research-list-empty">暂无生成记录。</p>
|
||||||
disabled={!historyPage}
|
)}
|
||||||
onClick={() => setHistoryPage(historyPage - 1)}
|
</>
|
||||||
|
)}
|
||||||
|
</ResearchList>
|
||||||
|
<ResearchDetail
|
||||||
|
title={
|
||||||
|
detailMode === "experiment"
|
||||||
|
? experiment?.name || "生成记录"
|
||||||
|
: page === "variants"
|
||||||
|
? "Alpha 变体详情"
|
||||||
|
: asset?.name || "新建模板"
|
||||||
|
}
|
||||||
|
visible={active && detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
>
|
>
|
||||||
上一页记录
|
{error && <Banner type="danger" description={error} />}
|
||||||
</Button>
|
{detailMode === "experiment" ? (
|
||||||
<Button
|
experiment && (
|
||||||
size="small"
|
<ExperimentView
|
||||||
disabled={(historyPage + 1) * 25 >= historyTotal}
|
key={experiment.id}
|
||||||
onClick={() => setHistoryPage(historyPage + 1)}
|
experiment={experiment}
|
||||||
>
|
onAction={onAction}
|
||||||
下一页记录
|
/>
|
||||||
</Button>
|
)
|
||||||
</div>
|
) : (
|
||||||
</aside>
|
|
||||||
<div className="research-main">
|
<div className="research-main">
|
||||||
<section className="research-card">
|
<section className="research-card">
|
||||||
<h3>研究输入与假设</h3>
|
<h3>研究输入与假设</h3>
|
||||||
@@ -694,7 +795,8 @@ export function ResearchWorkspace({
|
|||||||
<ComparisonPanel key={parent} baseline={parent} />
|
<ComparisonPanel key={parent} baseline={parent} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</ResearchDetail>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,25 @@
|
|||||||
.research-workspace {
|
.research-workspace {
|
||||||
height: 100%;
|
display: flex;
|
||||||
overflow: auto;
|
flex-direction: column;
|
||||||
padding: 24px 28px;
|
flex: 1;
|
||||||
color: var(--semi-color-text-0);
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--semi-color-text-0);
|
||||||
}
|
}
|
||||||
.research-page-heading,
|
|
||||||
.research-section-heading {
|
.research-section-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
.research-page-heading {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.research-page-heading h2 {
|
|
||||||
font-size: 22px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
|
||||||
.research-workspace p,
|
.research-workspace p,
|
||||||
|
.research-detail-content p,
|
||||||
.research-hint {
|
.research-hint {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--semi-color-text-2);
|
color: var(--semi-color-text-2);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
.research-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 220px minmax(0, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.research-library {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
max-height: calc(100vh - 180px);
|
|
||||||
overflow: auto;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
|
||||||
.research-library h3,
|
|
||||||
.research-card h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0 0 12px;
|
|
||||||
}
|
|
||||||
.research-library > h3 {
|
|
||||||
margin-top: 28px;
|
|
||||||
}
|
|
||||||
.research-library-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
.research-library-item.selected,
|
|
||||||
.research-library-item:hover {
|
|
||||||
background: var(--semi-color-fill-0);
|
|
||||||
}
|
|
||||||
.research-library-item span {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--semi-color-text-2);
|
|
||||||
}
|
|
||||||
.research-main {
|
.research-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -80,6 +32,10 @@
|
|||||||
background: var(--semi-color-bg-1);
|
background: var(--semi-color-bg-1);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.research-card h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
.research-card label,
|
.research-card label,
|
||||||
.research-editor label {
|
.research-editor label {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -127,13 +83,15 @@
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
.research-code-editor textarea,
|
.research-code-editor textarea,
|
||||||
.research-workspace code {
|
.research-workspace code,
|
||||||
|
.research-detail-content code {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.research-workspace pre {
|
.research-workspace pre,
|
||||||
|
.research-detail-content pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -170,19 +128,16 @@
|
|||||||
.research-table details {
|
.research-table details {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.research-workspace summary {
|
.research-workspace summary,
|
||||||
|
.research-detail-content summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.research-comparison-grid,
|
.research-comparison-grid {
|
||||||
.research-operator-layout {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
.research-operator-layout {
|
|
||||||
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
|
|
||||||
}
|
|
||||||
.research-comparison-grid article {
|
.research-comparison-grid article {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
}
|
}
|
||||||
@@ -192,31 +147,10 @@
|
|||||||
padding: 18px 12px 8px;
|
padding: 18px 12px 8px;
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.research-layout {
|
|
||||||
grid-template-columns: 170px minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.research-workspace {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.research-operator-layout,
|
|
||||||
.research-comparison-grid {
|
.research-comparison-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 760px) {
|
|
||||||
.research-layout {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.research-library {
|
|
||||||
position: static;
|
|
||||||
max-height: 200px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.research-page-heading {
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.research-sr-only {
|
.research-sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -381,3 +315,77 @@
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lists inherit the viewport boundary; controls and pagination never join data scrolling. */
|
||||||
|
.research-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.research-list-filters,
|
||||||
|
.research-list-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.research-list-filters {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.research-list-filters > .semi-input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.research-list-filters > .research-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.research-list .research-toolbar {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.research-list-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
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;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
.research-detail-content {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.research-detail-content .research-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.quantflow-detail-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.quantflow-detail-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px 0 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -240,6 +240,9 @@ label:not(.semi-checkbox):not(.semi-radio) {
|
|||||||
.primary-filters > .semi-select {
|
.primary-filters > .semi-select {
|
||||||
width: 136px;
|
width: 136px;
|
||||||
}
|
}
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
.advanced-filters {
|
.advanced-filters {
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ test("目录筛选、双层详情、完整输入、排除、备注与刷新", as
|
|||||||
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
|
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
|
||||||
await expect(fields).toBeVisible();
|
await expect(fields).toBeVisible();
|
||||||
await expect(fields).toContainText("全部 123 个字段");
|
await expect(fields).toContainText("全部 123 个字段");
|
||||||
expect((await fields.boundingBox())!.width).toBeCloseTo(1080, 0);
|
expect((await fields.boundingBox())!.width).toBeCloseTo(864, 0);
|
||||||
await page.getByLabel("搜索字段").fill("字段 12");
|
await page.getByLabel("搜索字段").fill("字段 12");
|
||||||
await expect(fields.getByRole("button", { name: /TEST 字段/ })).toHaveCount(
|
await expect(fields.getByRole("button", { name: /TEST 字段/ })).toHaveCount(
|
||||||
3,
|
3,
|
||||||
@@ -99,7 +99,7 @@ test("目录筛选、双层详情、完整输入、排除、备注与刷新", as
|
|||||||
.click();
|
.click();
|
||||||
const detail = page.getByRole("dialog", { name: "字段详情", exact: true });
|
const detail = page.getByRole("dialog", { name: "字段详情", exact: true });
|
||||||
await expect(detail).toContainText("FUTURE_TYPE");
|
await expect(detail).toContainText("FUTURE_TYPE");
|
||||||
expect((await detail.boundingBox())!.width).toBeCloseTo(432, 0);
|
expect((await detail.boundingBox())!.width).toBeCloseTo(864, 0);
|
||||||
await page.getByLabel("研究备注", { exact: true }).fill("保留字段研究假设");
|
await page.getByLabel("研究备注", { exact: true }).fill("保留字段研究假设");
|
||||||
await page.getByRole("button", { name: "保存备注", exact: true }).click();
|
await page.getByRole("button", { name: "保存备注", exact: true }).click();
|
||||||
await expect(page.getByText("研究备注已保存")).toBeVisible();
|
await expect(page.getByText("研究备注已保存")).toBeVisible();
|
||||||
@@ -173,7 +173,7 @@ test("窄屏抽屉、键盘隔离和研究范围切换", async ({ page }) => {
|
|||||||
await openFields(page);
|
await openFields(page);
|
||||||
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
|
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
|
||||||
await expect(fields).toBeVisible();
|
await expect(fields).toBeVisible();
|
||||||
expect((await fields.boundingBox())!.width).toBeCloseTo(390, 0);
|
expect((await fields.boundingBox())!.width).toBeCloseTo(234, 0);
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: "TEST 字段 000", exact: true })
|
.getByRole("button", { name: "TEST 字段 000", exact: true })
|
||||||
.click();
|
.click();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
|
|||||||
.getByRole("button", { name: "QuantFlow", exact: true })
|
.getByRole("button", { name: "QuantFlow", exact: true })
|
||||||
.click();
|
.click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "QuantFlow", exact: true }),
|
page.getByRole("region", { name: "QuantFlow", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page.request.put("/api/v1/account/credentials", {
|
await page.request.put("/api/v1/account/credentials", {
|
||||||
headers,
|
headers,
|
||||||
@@ -114,6 +114,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
|
|||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "新建流程", exact: true }).click();
|
||||||
await page.getByLabel("流程名称", { exact: true }).fill("画布原生研究");
|
await page.getByLabel("流程名称", { exact: true }).fill("画布原生研究");
|
||||||
async function add(kind: string, label: string) {
|
async function add(kind: string, label: string) {
|
||||||
await page.getByLabel("新增节点类型").selectOption(kind);
|
await page.getByLabel("新增节点类型").selectOption(kind);
|
||||||
@@ -179,7 +180,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
|
|||||||
.getByRole("option")
|
.getByRole("option")
|
||||||
.filter({ hasText: fixed.id.slice(0, 8) })
|
.filter({ hasText: fixed.id.slice(0, 8) })
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole("heading", { name: "QuantFlow", exact: true }).click();
|
await page.getByLabel("自动研究假设").click();
|
||||||
for (const [label, value] of [
|
for (const [label, value] of [
|
||||||
["最大轮数", "1"],
|
["最大轮数", "1"],
|
||||||
["最大模拟条目数", "2"],
|
["最大模拟条目数", "2"],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
|
|||||||
.getByRole("button", { name: "研究流水线", exact: true })
|
.getByRole("button", { name: "研究流水线", exact: true })
|
||||||
.click();
|
.click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "研究流水线", exact: true }),
|
page.getByRole("region", { name: "研究流水线", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page.request.put("/api/v1/account/credentials", {
|
await page.request.put("/api/v1/account/credentials", {
|
||||||
headers,
|
headers,
|
||||||
@@ -94,6 +94,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
|
|||||||
).ok(),
|
).ok(),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "新建自动研究", exact: true }).click();
|
||||||
await page.getByLabel("自动研究名称").fill("浏览器有限研究");
|
await page.getByLabel("自动研究名称").fill("浏览器有限研究");
|
||||||
await page.getByLabel("自动研究假设").fill("研究排名稳定性");
|
await page.getByLabel("自动研究假设").fill("研究排名稳定性");
|
||||||
await page.getByRole("combobox", { name: "自动研究固定输入" }).click();
|
await page.getByRole("combobox", { name: "自动研究固定输入" }).click();
|
||||||
@@ -101,7 +102,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
|
|||||||
.getByRole("option")
|
.getByRole("option")
|
||||||
.filter({ hasText: fixed.id.slice(0, 8) })
|
.filter({ hasText: fixed.id.slice(0, 8) })
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole("heading", { name: "研究流水线", exact: true }).click();
|
await page.getByLabel("自动研究名称").click();
|
||||||
for (const [label, value] of [
|
for (const [label, value] of [
|
||||||
["最大轮数", "1"],
|
["最大轮数", "1"],
|
||||||
["最大模拟条目数", "2"],
|
["最大模拟条目数", "2"],
|
||||||
@@ -110,9 +111,9 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
|
|||||||
])
|
])
|
||||||
await page.getByLabel(label, { exact: true }).fill(value);
|
await page.getByLabel(label, { exact: true }).fill(value);
|
||||||
await page.getByRole("button", { name: "核对并启动研究" }).click();
|
await page.getByRole("button", { name: "核对并启动研究" }).click();
|
||||||
await expect(page.getByRole("dialog", {name: "确认自动研究授权"})).toContainText(
|
await expect(
|
||||||
"最多 1 轮、2 条模拟、2 次模型调用",
|
page.getByRole("dialog", { name: "确认自动研究授权" }),
|
||||||
);
|
).toContainText("最多 1 轮、2 条模拟、2 次模型调用");
|
||||||
const started = page.waitForResponse(
|
const started = page.waitForResponse(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.url().endsWith("/research/flows/runs") &&
|
r.url().endsWith("/research/flows/runs") &&
|
||||||
@@ -143,6 +144,9 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
|
|||||||
expect(completed.model_calls_used).toBe(2);
|
expect(completed.model_calls_used).toBe(2);
|
||||||
expect(completed.steps).toHaveLength(8);
|
expect(completed.steps).toHaveLength(8);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "浏览器有限研究", exact: true })
|
||||||
|
.click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("region", { name: "研究运行详情" }),
|
page.getByRole("region", { name: "研究运行详情" }),
|
||||||
).toContainText("浏览器有限研究");
|
).toContainText("浏览器有限研究");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
||||||
await page.getByRole("button", { name: "进入工作空间" }).click();
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "特征工程", exact: true }),
|
page.getByRole("button", { name: "新建特征方案", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page.request.put("/api/v1/account/credentials", {
|
await page.request.put("/api/v1/account/credentials", {
|
||||||
headers,
|
headers,
|
||||||
@@ -62,6 +62,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
).json();
|
).json();
|
||||||
expect(fixed.id).toBeTruthy();
|
expect(fixed.id).toBeTruthy();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "新建特征方案", exact: true }).click();
|
||||||
await page.getByLabel("特征方案名称", { exact: true }).fill("浏览器特征验收");
|
await page.getByLabel("特征方案名称", { exact: true }).fill("浏览器特征验收");
|
||||||
await page
|
await page
|
||||||
.getByLabel("特征经济假设", { exact: true })
|
.getByLabel("特征经济假设", { exact: true })
|
||||||
@@ -71,7 +72,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
.getByRole("option")
|
.getByRole("option")
|
||||||
.filter({ hasText: fixed.id.slice(0, 8) })
|
.filter({ hasText: fixed.id.slice(0, 8) })
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole("heading", { name: "特征工程", exact: true }).click();
|
await page.getByLabel("特征方案名称", { exact: true }).click();
|
||||||
await page.getByRole("button", { name: "添加处理步骤" }).click();
|
await page.getByRole("button", { name: "添加处理步骤" }).click();
|
||||||
await page.getByLabel("步骤 1 名称").fill("横截面排名");
|
await page.getByLabel("步骤 1 名称").fill("横截面排名");
|
||||||
await page.getByLabel("步骤 1 理由").fill("减少尺度影响");
|
await page.getByLabel("步骤 1 理由").fill("减少尺度影响");
|
||||||
@@ -97,6 +98,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
expect(templates.items[0].provenance.feature.content.input_ids).toEqual([
|
expect(templates.items[0].provenance.feature.content.input_ids).toEqual([
|
||||||
fixed.id,
|
fixed.id,
|
||||||
]);
|
]);
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
||||||
await page.getByLabel("视图名称", { exact: true }).fill("浏览器常用队列");
|
await page.getByLabel("视图名称", { exact: true }).fill("浏览器常用队列");
|
||||||
await page.getByRole("button", { name: "保存为新视图" }).click();
|
await page.getByRole("button", { name: "保存为新视图" }).click();
|
||||||
|
|||||||
Reference in New Issue
Block a user