feat(agent): render local interactive answers and fold completed process
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
|
||||
const block = {
|
||||
version: "answer-ui@1",
|
||||
block_id: "pressure",
|
||||
fallback_text: "演练压力读数,仍待现场核验。",
|
||||
spec: {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
type: "Stack",
|
||||
props: { columns: "one", gap: "normal" },
|
||||
children: ["metrics", "chart", "evidence", "location", "form"]
|
||||
},
|
||||
metrics: {
|
||||
type: "Stack",
|
||||
props: { columns: "two", gap: "compact" },
|
||||
children: ["pressure", "flow"]
|
||||
},
|
||||
pressure: {
|
||||
type: "Metric",
|
||||
props: { label: "压力", value: "0.30", unit: "MPa", source: "演练数据" },
|
||||
children: []
|
||||
},
|
||||
flow: {
|
||||
type: "Metric",
|
||||
props: { label: "流量", value: "18", unit: "m³/h", source: "演练数据" },
|
||||
children: []
|
||||
},
|
||||
chart: {
|
||||
type: "Chart",
|
||||
props: {
|
||||
title: "演练监测趋势",
|
||||
chart_type: "line",
|
||||
x_data: ["08:00", "09:00"],
|
||||
series: [{ name: "压力(MPa)", data: [0.3, 0.28] }],
|
||||
source: "浏览器验收演练数据"
|
||||
},
|
||||
children: []
|
||||
},
|
||||
evidence: {
|
||||
type: "Evidence",
|
||||
props: {
|
||||
title: "查看演练证据",
|
||||
summary: "仅用于组件验收,不代表已核验现场事实。",
|
||||
source: "浏览器演练",
|
||||
evidence_id: "DEMO-001",
|
||||
observed_at: null,
|
||||
kind: "demo"
|
||||
},
|
||||
children: []
|
||||
},
|
||||
location: {
|
||||
type: "MapLocation",
|
||||
props: {
|
||||
label: "定位演练监测点",
|
||||
layer: "scada",
|
||||
feature_ids: ["DEMO-P001"],
|
||||
source: "演练监测点"
|
||||
},
|
||||
children: []
|
||||
},
|
||||
form: {
|
||||
type: "FollowUpForm",
|
||||
props: {
|
||||
title: "补充核验资料",
|
||||
description: "填写后由你主动发送给 Agent。",
|
||||
fields: [{ name: "location", label: "现场位置", kind: "text", required: true }]
|
||||
},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const width of [1440, 375, 320]) {
|
||||
test(`local rich answers stream, restore and submit at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await mockWorkbenchContext(page);
|
||||
await mockAgentApi(page, {
|
||||
sessions: [
|
||||
{
|
||||
session_id: "browser-session-1",
|
||||
title: "演练结构化回答",
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now()
|
||||
}
|
||||
]
|
||||
});
|
||||
const parts = [
|
||||
{ type: "text", text: "以下资料均为演练数据。" },
|
||||
{ type: "data-answer_ui", id: "pressure", data: block },
|
||||
{ type: "text", text: "仍需用户补充现场核验资料。" },
|
||||
{
|
||||
type: "data-final_answer",
|
||||
data: { content: "仍需用户补充现场核验资料。", ui_block_ids: ["pressure"] }
|
||||
}
|
||||
];
|
||||
await page.route("**/api/v1/agent/chat/session/browser-session-1", async (route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
await route.fulfill({
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
json: {
|
||||
session_id: "browser-session-1",
|
||||
title: "演练结构化回答",
|
||||
purpose: "ordinary_chat",
|
||||
status: "active",
|
||||
is_streaming: false,
|
||||
messages: [
|
||||
{
|
||||
id: "user-fixture",
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "查看演练监测资料" }]
|
||||
},
|
||||
{ id: "rich-answer", role: "assistant", parts }
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
const prompts: string[] = [];
|
||||
await page.route("**/api/v1/agent/chat/stream", async (route) => {
|
||||
if (route.request().method() === "OPTIONS") return route.fallback();
|
||||
const body = route.request().postDataJSON() as {
|
||||
messages: Array<{ role: string; parts: Array<{ type: string; text?: string }> }>;
|
||||
};
|
||||
const text =
|
||||
body.messages
|
||||
.filter((m) => m.role === "user")
|
||||
.at(-1)
|
||||
?.parts.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("") ?? "";
|
||||
prompts.push(text);
|
||||
const chunks =
|
||||
prompts.length === 1
|
||||
? [
|
||||
{ type: "start", messageId: "rich-answer" },
|
||||
{ type: "text-start", id: "answer" },
|
||||
{ type: "text-delta", id: "answer", delta: parts[0].text },
|
||||
{ type: "text-end", id: "answer" },
|
||||
{ ...parts[1], data: { ...block, fallback_text: "演练初始数据" } },
|
||||
parts[1],
|
||||
{ type: "text-start", id: "answer-1" },
|
||||
{ type: "text-delta", id: "answer-1", delta: parts[2].text },
|
||||
{ type: "text-end", id: "answer-1" },
|
||||
parts[3],
|
||||
{ type: "finish", finishReason: "stop" }
|
||||
]
|
||||
: [
|
||||
{ type: "start", messageId: "form-answer" },
|
||||
{ type: "text-start", id: "answer" },
|
||||
{ type: "text-delta", id: "answer", delta: "已收到补充资料,尚未保存业务证据。" },
|
||||
{ type: "text-end", id: "answer" },
|
||||
{ type: "finish", finishReason: "stop" }
|
||||
];
|
||||
await route.fulfill({
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "text/event-stream",
|
||||
"X-Vercel-AI-UI-Message-Stream": "v1"
|
||||
},
|
||||
body: chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")
|
||||
});
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page
|
||||
.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ })
|
||||
.click();
|
||||
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||
await panel.locator("textarea").fill("查看演练监测资料");
|
||||
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
||||
const ui = panel.locator('[data-answer-ui="pressure"]');
|
||||
await expect(ui).toHaveCount(1);
|
||||
const process = panel
|
||||
.locator("details")
|
||||
.filter({ has: page.locator("summary", { hasText: "查看处理过程" }) });
|
||||
await expect(process).not.toHaveAttribute("open");
|
||||
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeHidden();
|
||||
await process.locator("summary").click();
|
||||
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeVisible();
|
||||
await process.locator("summary").click();
|
||||
if (width >= 1024) {
|
||||
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
||||
for (const panelWidth of [400, 640]) {
|
||||
const box = (await handle.boundingBox())!;
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(panelWidth, box.y + box.height / 2);
|
||||
await page.mouse.up();
|
||||
await expect(panel).toHaveCSS("width", `${panelWidth}px`);
|
||||
const columns = await ui
|
||||
.locator(".answer-ui-stack-comparison")
|
||||
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(" ").length);
|
||||
expect(columns).toBe(panelWidth === 640 ? 2 : 1);
|
||||
}
|
||||
}
|
||||
await expect(ui.getByText("0.30", { exact: false }).first()).toBeVisible();
|
||||
await ui.getByRole("button", { name: "数据", exact: true }).click();
|
||||
await expect(ui.getByRole("table")).toContainText("0.28");
|
||||
const evidence = ui.locator("summary", { hasText: "查看演练证据" });
|
||||
await evidence.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(ui.getByText("仅用于组件验收,不代表已核验现场事实。")).toBeVisible();
|
||||
await expect(panel.getByText("仍需用户补充现场核验资料。", { exact: true })).toHaveCount(1);
|
||||
const bounds = await ui.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const panel = element.closest("aside")!.getBoundingClientRect();
|
||||
return rect.left >= panel.left && rect.right <= panel.right;
|
||||
});
|
||||
expect(bounds).toBe(true);
|
||||
await ui.getByRole("table").scrollIntoViewIfNeeded();
|
||||
await panel.screenshot({ path: `test-results/answer-ui-${width}.png` });
|
||||
// Restore from canonical persisted parts through the actual history selector.
|
||||
await panel.getByRole("button", { name: /历史/ }).click();
|
||||
await page.getByRole("button", { name: /演练结构化回答/ }).click();
|
||||
await expect(ui).toHaveCount(1);
|
||||
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("");
|
||||
expect(prompts).toHaveLength(1);
|
||||
await ui.getByLabel("现场位置(必填)").fill("演练管段东侧");
|
||||
await ui.getByRole("button", { name: "发送补充资料" }).click();
|
||||
await expect.poll(() => prompts.length).toBe(2);
|
||||
expect(prompts[1]).toContain("现场位置:演练管段东侧");
|
||||
await expect(
|
||||
panel.getByText("已收到补充资料,尚未保存业务证据。", { exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user