-
- {error &&
}
-
-
);
}
diff --git a/frontend/src/research/workspace.css b/frontend/src/research/workspace.css
index 43fbf68..24864ab 100644
--- a/frontend/src/research/workspace.css
+++ b/frontend/src/research/workspace.css
@@ -1,73 +1,25 @@
.research-workspace {
- height: 100%;
- overflow: auto;
- padding: 24px 28px;
- color: var(--semi-color-text-0);
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
min-width: 0;
+ overflow: hidden;
+ color: var(--semi-color-text-0);
}
-.research-page-heading,
.research-section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
-.research-page-heading {
- margin-bottom: 24px;
-}
-.research-page-heading h2 {
- font-size: 22px;
- margin: 0 0 8px;
-}
.research-workspace p,
+.research-detail-content p,
.research-hint {
font-size: 13px;
color: var(--semi-color-text-2);
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 {
min-width: 0;
display: grid;
@@ -80,6 +32,10 @@
background: var(--semi-color-bg-1);
min-width: 0;
}
+.research-card h3 {
+ font-size: 16px;
+ margin: 0 0 12px;
+}
.research-card label,
.research-editor label {
display: flex;
@@ -127,13 +83,15 @@
justify-self: start;
}
.research-code-editor textarea,
-.research-workspace code {
+.research-workspace code,
+.research-detail-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
-.research-workspace pre {
+.research-workspace pre,
+.research-detail-content pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
font-size: 12px;
@@ -170,19 +128,16 @@
.research-table details {
margin-top: 8px;
}
-.research-workspace summary {
+.research-workspace summary,
+.research-detail-content summary {
cursor: pointer;
font-size: 13px;
}
-.research-comparison-grid,
-.research-operator-layout {
+.research-comparison-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
-.research-operator-layout {
- grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
-}
.research-comparison-grid article {
padding-top: 12px;
}
@@ -192,31 +147,10 @@
padding: 18px 12px 8px;
}
@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 {
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 {
position: absolute;
@@ -381,3 +315,77 @@
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;
+}
diff --git a/frontend/src/style.css b/frontend/src/style.css
index ae18188..077f9ab 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -240,6 +240,9 @@ label:not(.semi-checkbox):not(.semi-radio) {
.primary-filters > .semi-select {
width: 136px;
}
+.filter-actions {
+ margin-top: var(--space-2);
+}
.advanced-filters {
margin-top: var(--space-2);
}
diff --git a/frontend/tests/dataset.spec.ts b/frontend/tests/dataset.spec.ts
index 07627b8..f0b6882 100644
--- a/frontend/tests/dataset.spec.ts
+++ b/frontend/tests/dataset.spec.ts
@@ -89,7 +89,7 @@ test("目录筛选、双层详情、完整输入、排除、备注与刷新", as
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
await expect(fields).toBeVisible();
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 expect(fields.getByRole("button", { name: /TEST 字段/ })).toHaveCount(
3,
@@ -99,7 +99,7 @@ test("目录筛选、双层详情、完整输入、排除、备注与刷新", as
.click();
const detail = page.getByRole("dialog", { name: "字段详情", exact: true });
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.getByRole("button", { name: "保存备注", exact: true }).click();
await expect(page.getByText("研究备注已保存")).toBeVisible();
@@ -173,7 +173,7 @@ test("窄屏抽屉、键盘隔离和研究范围切换", async ({ page }) => {
await openFields(page);
const fields = page.getByRole("dialog", { name: "数据字段", exact: true });
await expect(fields).toBeVisible();
- expect((await fields.boundingBox())!.width).toBeCloseTo(390, 0);
+ expect((await fields.boundingBox())!.width).toBeCloseTo(234, 0);
await page
.getByRole("button", { name: "TEST 字段 000", exact: true })
.click();
diff --git a/frontend/tests/quantflow.spec.ts b/frontend/tests/quantflow.spec.ts
index 0c121ac..43acf5f 100644
--- a/frontend/tests/quantflow.spec.ts
+++ b/frontend/tests/quantflow.spec.ts
@@ -19,7 +19,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
.getByRole("button", { name: "QuantFlow", exact: true })
.click();
await expect(
- page.getByRole("heading", { name: "QuantFlow", exact: true }),
+ page.getByRole("region", { name: "QuantFlow", exact: true }),
).toBeVisible();
await page.request.put("/api/v1/account/credentials", {
headers,
@@ -114,6 +114,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
})
).json();
await page.reload();
+ await page.getByRole("button", { name: "新建流程", exact: true }).click();
await page.getByLabel("流程名称", { exact: true }).fill("画布原生研究");
async function add(kind: string, label: string) {
await page.getByLabel("新增节点类型").selectOption(kind);
@@ -179,7 +180,7 @@ test("native QuantFlow canvas saves versions, branches and restores artifacts",
.getByRole("option")
.filter({ hasText: fixed.id.slice(0, 8) })
.click();
- await page.getByRole("heading", { name: "QuantFlow", exact: true }).click();
+ await page.getByLabel("自动研究假设").click();
for (const [label, value] of [
["最大轮数", "1"],
["最大模拟条目数", "2"],
diff --git a/frontend/tests/research-flows.spec.ts b/frontend/tests/research-flows.spec.ts
index dfc8e84..49c5498 100644
--- a/frontend/tests/research-flows.spec.ts
+++ b/frontend/tests/research-flows.spec.ts
@@ -19,7 +19,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
.getByRole("button", { name: "研究流水线", exact: true })
.click();
await expect(
- page.getByRole("heading", { name: "研究流水线", exact: true }),
+ page.getByRole("region", { name: "研究流水线", exact: true }),
).toBeVisible();
await page.request.put("/api/v1/account/credentials", {
headers,
@@ -94,6 +94,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
).ok(),
).toBe(true);
await page.reload();
+ await page.getByRole("button", { name: "新建自动研究", exact: true }).click();
await page.getByLabel("自动研究名称").fill("浏览器有限研究");
await page.getByLabel("自动研究假设").fill("研究排名稳定性");
await page.getByRole("combobox", { name: "自动研究固定输入" }).click();
@@ -101,7 +102,7 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
.getByRole("option")
.filter({ hasText: fixed.id.slice(0, 8) })
.click();
- await page.getByRole("heading", { name: "研究流水线", exact: true }).click();
+ await page.getByLabel("自动研究名称").click();
for (const [label, value] of [
["最大轮数", "1"],
["最大模拟条目数", "2"],
@@ -110,16 +111,16 @@ test("finite pipeline confirms scope and budget, executes native steps and resto
])
await page.getByLabel(label, { exact: true }).fill(value);
await page.getByRole("button", { name: "核对并启动研究" }).click();
- await expect(page.getByRole("dialog", {name: "确认自动研究授权"})).toContainText(
- "最多 1 轮、2 条模拟、2 次模型调用",
- );
+ await expect(
+ page.getByRole("dialog", { name: "确认自动研究授权" }),
+ ).toContainText("最多 1 轮、2 条模拟、2 次模型调用");
const started = page.waitForResponse(
(r) =>
r.url().endsWith("/research/flows/runs") &&
r.request().method() === "POST",
);
await page
- .getByRole("dialog", {name: "确认自动研究授权"})
+ .getByRole("dialog", { name: "确认自动研究授权" })
.getByRole("button", { name: "confirm", exact: true })
.click();
const response = await started;
@@ -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.steps).toHaveLength(8);
await page.reload();
+ await page
+ .getByRole("button", { name: "浏览器有限研究", exact: true })
+ .click();
await expect(
page.getByRole("region", { name: "研究运行详情" }),
).toContainText("浏览器有限研究");
diff --git a/frontend/tests/research-outcomes.spec.ts b/frontend/tests/research-outcomes.spec.ts
index 71b4b8d..cc7679b 100644
--- a/frontend/tests/research-outcomes.spec.ts
+++ b/frontend/tests/research-outcomes.spec.ts
@@ -15,7 +15,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
await page.getByRole("button", { name: "进入工作空间" }).click();
await expect(
- page.getByRole("heading", { name: "特征工程", exact: true }),
+ page.getByRole("button", { name: "新建特征方案", exact: true }),
).toBeVisible();
await page.request.put("/api/v1/account/credentials", {
headers,
@@ -62,6 +62,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
).json();
expect(fixed.id).toBeTruthy();
await page.reload();
+ await page.getByRole("button", { name: "新建特征方案", exact: true }).click();
await page.getByLabel("特征方案名称", { exact: true }).fill("浏览器特征验收");
await page
.getByLabel("特征经济假设", { exact: true })
@@ -71,7 +72,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
.getByRole("option")
.filter({ hasText: fixed.id.slice(0, 8) })
.click();
- await page.getByRole("heading", { name: "特征工程", exact: true }).click();
+ await page.getByLabel("特征方案名称", { exact: true }).click();
await page.getByRole("button", { name: "添加处理步骤" }).click();
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([
fixed.id,
]);
+ await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
await page.getByLabel("视图名称", { exact: true }).fill("浏览器常用队列");
await page.getByRole("button", { name: "保存为新视图" }).click();