fix(web): 连续展示聊天备选项编号
This commit is contained in:
parent
3842250d14
commit
263af1ee32
|
|
@ -0,0 +1,84 @@
|
||||||
|
// ============================================================
|
||||||
|
// 计划员聊天编号回归:模型常把多个选项都写成 1.,界面必须连续显示 1/2/3。
|
||||||
|
// 全部离线:只桩会话历史,不连接现场后端。
|
||||||
|
// ============================================================
|
||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const json = (body: unknown, status = 200) => ({
|
||||||
|
status, contentType: 'application/json', body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function stubApi(page: Page): Promise<void> {
|
||||||
|
const message = {
|
||||||
|
role: 'agent',
|
||||||
|
text: [
|
||||||
|
'接下来你有三个选择:',
|
||||||
|
'',
|
||||||
|
'1. 修改现有订单',
|
||||||
|
'',
|
||||||
|
'把数量改成 50 套、交期改成 10 月 14 日。',
|
||||||
|
'',
|
||||||
|
'1. 新增一张急单',
|
||||||
|
'',
|
||||||
|
'换一个新订单号,按你给的数量和交期录入。',
|
||||||
|
'',
|
||||||
|
'1. 直接使用已有急单',
|
||||||
|
'',
|
||||||
|
'按现有急单的交期评估插入后的影响。',
|
||||||
|
].join('\n'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.route((url: URL) => url.pathname.startsWith('/api/'), route => {
|
||||||
|
const path = new URL(route.request().url()).pathname;
|
||||||
|
switch (path) {
|
||||||
|
case '/api/health':
|
||||||
|
return route.fulfill(json({ interfaceVersion: '1.0', authProvider: 'local', licenseProvider: 'local' }));
|
||||||
|
case '/api/auth/me':
|
||||||
|
return route.fulfill(json({
|
||||||
|
user: {
|
||||||
|
user_id: 'user-1', username: 'e2e', fullname: 'E2E', tenant_uuid: 'tenant-1',
|
||||||
|
roles: ['admin'], auth_kind: 'user',
|
||||||
|
},
|
||||||
|
provider: 'local',
|
||||||
|
license: null,
|
||||||
|
}));
|
||||||
|
case '/api/features':
|
||||||
|
return route.fulfill(json({ version: 1, source: 'default', path: '', error: null, unknown: [], features: {} }));
|
||||||
|
case '/api/workspace':
|
||||||
|
return route.fulfill(json({
|
||||||
|
projects: [],
|
||||||
|
sessions: [{
|
||||||
|
id: 'session-1', projectId: '__personal__', title: '编号测试', status: 'done',
|
||||||
|
updatedAt: '2026-09-18 10:00:00', scope: 'personal',
|
||||||
|
}],
|
||||||
|
files: [],
|
||||||
|
messages: { 'session-1': [message] },
|
||||||
|
activeProjectId: '__personal__', activeSessionId: 'session-1',
|
||||||
|
}));
|
||||||
|
case '/api/sessions/session-1/messages':
|
||||||
|
return route.fulfill(json({ sessionId: 'session-1', messages: [message] }));
|
||||||
|
case '/api/skills':
|
||||||
|
return route.fulfill(json({ skills: [], skillsDir: '', mode: 'web' }));
|
||||||
|
case '/api/skills/health':
|
||||||
|
return route.fulfill(json({ health: [] }));
|
||||||
|
default:
|
||||||
|
return route.fulfill(json({ detail: 'e2e stub' }, 404));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('多个备选项即使都写成 1.,页面也连续显示 1、2、3', async ({ page }) => {
|
||||||
|
await stubApi(page);
|
||||||
|
await page.goto('/');
|
||||||
|
const chat = page.locator('section.chat[data-session-ready="true"]');
|
||||||
|
await expect(chat).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
const items = chat.locator('.chat-md ol.md-list > li');
|
||||||
|
await expect(items).toHaveCount(3);
|
||||||
|
await expect(items.nth(0)).toContainText('修改现有订单');
|
||||||
|
await expect(items.nth(1)).toContainText('新增一张急单');
|
||||||
|
await expect(items.nth(2)).toContainText('直接使用已有急单');
|
||||||
|
|
||||||
|
const values = await items.evaluateAll(nodes => nodes.map(node => (node as HTMLLIElement).value));
|
||||||
|
expect(values).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
@ -37,7 +37,10 @@ export function ChatMarkdown(props: { text: string; streaming?: boolean }) {
|
||||||
const raw = props.text.replace(/\r\n/g, '\n');
|
const raw = props.text.replace(/\r\n/g, '\n');
|
||||||
const lines = raw.split('\n');
|
const lines = raw.split('\n');
|
||||||
const blocks: ReactNode[] = [];
|
const blocks: ReactNode[] = [];
|
||||||
let list: { ordered: boolean; items: string[] } | null = null;
|
let list: { ordered: boolean; items: { text: string; value?: number }[] } | null = null;
|
||||||
|
// Pi 常把同一段回复里的多个备选项拆成几张短列表,模型又会把每张都写成 1.
|
||||||
|
// 对计划员来说这是一个连续选择,所以跨空行/说明段落继续编号;遇到新标题再重置。
|
||||||
|
let orderedCursor: number | null = null;
|
||||||
let key = 0;
|
let key = 0;
|
||||||
|
|
||||||
const flushList = () => {
|
const flushList = () => {
|
||||||
|
|
@ -46,7 +49,7 @@ export function ChatMarkdown(props: { text: string; streaming?: boolean }) {
|
||||||
blocks.push(
|
blocks.push(
|
||||||
<Tag key={`l${key++}`} className="md-list">
|
<Tag key={`l${key++}`} className="md-list">
|
||||||
{list.items.map((it, i) => (
|
{list.items.map((it, i) => (
|
||||||
<li key={i}>{renderInline(it)}</li>
|
<li key={i} value={it.value}>{renderInline(it.text)}</li>
|
||||||
))}
|
))}
|
||||||
</Tag>,
|
</Tag>,
|
||||||
);
|
);
|
||||||
|
|
@ -94,6 +97,7 @@ export function ChatMarkdown(props: { text: string; streaming?: boolean }) {
|
||||||
const ol = /^(\d+)[.)、]\s+(.+)$/.exec(line);
|
const ol = /^(\d+)[.)、]\s+(.+)$/.exec(line);
|
||||||
if (heading) {
|
if (heading) {
|
||||||
flushList();
|
flushList();
|
||||||
|
orderedCursor = null;
|
||||||
const level = heading[1].length;
|
const level = heading[1].length;
|
||||||
blocks.push(<div key={`h${key++}`} className={`md-h md-h${level}`}>{renderInline(heading[2])}</div>);
|
blocks.push(<div key={`h${key++}`} className={`md-h md-h${level}`}>{renderInline(heading[2])}</div>);
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
@ -104,7 +108,7 @@ export function ChatMarkdown(props: { text: string; streaming?: boolean }) {
|
||||||
flushList();
|
flushList();
|
||||||
list = { ordered: false, items: [] };
|
list = { ordered: false, items: [] };
|
||||||
}
|
}
|
||||||
list.items.push(ul[1]);
|
list.items.push({ text: ul[1] });
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +117,9 @@ export function ChatMarkdown(props: { text: string; streaming?: boolean }) {
|
||||||
flushList();
|
flushList();
|
||||||
list = { ordered: true, items: [] };
|
list = { ordered: true, items: [] };
|
||||||
}
|
}
|
||||||
list.items.push(ol[2]);
|
const value: number = orderedCursor ?? Number(ol[1]);
|
||||||
|
orderedCursor = value + 1;
|
||||||
|
list.items.push({ text: ol[2], value });
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue