Merge Agent attachment panel and explicit delivered-file filtering
This commit is contained in:
commit
be53e44dcf
14 files changed
+1010
-98
No files matched your search
@@ -24,21 +24,31 @@ test.beforeAll(async () => {
|
||||
load(id) {
|
||||
if (id === entry) return `
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useState } from 'react';
|
||||
import { CloudChat } from '/src/pages/CloudAgents/CloudChat.tsx';
|
||||
import { CloudAvatar } from '/src/pages/CloudAgents/CloudAvatar.tsx';
|
||||
import avatar from '/src/assets/agent-avatars/avatar-05.png';
|
||||
import '/src/styles/globals.css';
|
||||
const examples = ['我想一起想一个有趣的故事。', '请帮我把今天的新发现整理一下。'];
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<main className="flex h-screen min-h-0 min-w-0 flex-col">
|
||||
const name = new URLSearchParams(location.search).has('longName') ? '我的故事搭档'.repeat(16) : '我的故事搭档';
|
||||
function Harness() {
|
||||
const [header, setHeader] = useState(null);
|
||||
return <main className="flex h-screen min-h-0 min-w-0 flex-col">
|
||||
<header className="agent-workspace-header agent-chat-workspace-header">
|
||||
<CloudAvatar src={avatar} name={name} className="agent-chat-header-avatar" />
|
||||
<h2 className="agent-chat-header-name" title={name}>{name}</h2>
|
||||
<div ref={setHeader} className="agent-chat-header-files" />
|
||||
</header>
|
||||
<CloudChat slug="ml-chat-layout-fixture" assistantName="我的故事搭档" assistantAvatarUrl={avatar}
|
||||
examples={examples} />
|
||||
</main>
|
||||
);
|
||||
examples={examples} attachmentHeader={header} />
|
||||
</main>;
|
||||
}
|
||||
createRoot(document.getElementById('root')).render(<Harness />);
|
||||
`;
|
||||
},
|
||||
configureServer(vite) {
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
if (req.url !== '/') return next();
|
||||
if (req.url?.split('?')[0] !== '/') return next();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<!doctype html><html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/__cloud-agent-chat-layout-harness.tsx"></script></body></html>');
|
||||
});
|
||||
@@ -57,6 +67,7 @@ const longWord = 'an-unbroken-story-notebook-'.repeat(24);
|
||||
const richReply = [
|
||||
'## 一起把小发现变成故事',
|
||||
'你可以先选一个最喜欢的发现,再让角色去试一试。',
|
||||
'[下载故事笔记](sandbox:/故事笔记.md)',
|
||||
`这个很长的名字也应该完整保留:${longWord}`,
|
||||
`[查看完整故事线索](https://example.invalid/story/${longWord})`,
|
||||
['| 今天的线索 | 可以接着想什么 |', '| --- | --- |',
|
||||
@@ -92,7 +103,7 @@ type FixtureSeed = { threads: CloudThread[]; histories: Record<string, CloudHist
|
||||
type FixtureStream = EventTarget & { readyState: number; url: string };
|
||||
type FixtureState = FixtureSeed & { calls: FixtureCall[]; attachments: CloudAttachment[]; streams: FixtureStream[] };
|
||||
type FixtureWindow = Window & { __cloudChatFixture: FixtureState };
|
||||
type FixtureKind = 'empty' | 'history' | 'running' | 'approval' | 'thinking';
|
||||
type FixtureKind = 'empty' | 'history' | 'running' | 'approval' | 'thinking' | 'delivery';
|
||||
|
||||
async function installFixture(page: Page, kind: FixtureKind) {
|
||||
const isRunning = kind === 'running' || kind === 'thinking';
|
||||
@@ -103,7 +114,14 @@ async function installFixture(page: Page, kind: FixtureKind) {
|
||||
const run = isRunning ? makeRun(first, 'running') : kind === 'approval' ? makeRun(first, 'interrupted', interrupt) : null;
|
||||
const seed: FixtureSeed = kind === 'empty' ? { threads: [], histories: {} } : {
|
||||
threads: [first, second], histories: {
|
||||
[first.thread_id]: kind === 'thinking' ? { ...makeHistory(first, false, run), messages: [] } : makeHistory(first, kind !== 'approval', run),
|
||||
[first.thread_id]: kind === 'thinking' ? { ...makeHistory(first, false, run), messages: [] }
|
||||
: kind === 'delivery' ? { ...makeHistory(first), messages: [
|
||||
{ id: 1, role: 'user', content: '[用户提到的文件](sandbox:/user.txt)', request_id: 'delivery-user', run_id: null, created_at: date },
|
||||
{ id: 2, role: 'assistant', request_id: 'delivery-answer', run_id: null, created_at: date,
|
||||
content: '[下载完成的小程序](sandbox:/deliverable.py)\n\n准备时用到了 /internal.py、/call.txt、/cache.json 和 /plain.txt。\n\n`[行内示例](sandbox:/inline.py)`\n\n```markdown\n[代码示例](sandbox:/example.py)\n```',
|
||||
reasoning_content: '[思考过程](sandbox:/thought.txt)',
|
||||
},
|
||||
] } : makeHistory(first, kind !== 'approval', run),
|
||||
[second.thread_id]: makeHistory(second),
|
||||
},
|
||||
};
|
||||
@@ -140,6 +158,7 @@ async function installFixture(page: Page, kind: FixtureKind) {
|
||||
if (path === '/api/cloud-agents/recovery') return result({ recent: null, pending: [] });
|
||||
if (path === '/api/cloud-agents/recovery/recent') return result({ saved: true });
|
||||
if (path === '/api/cloud-agents/attachments/pick') return result({ object_name: 'temporary/狐狸照片.png', file_name: '狐狸照片.png', file_type: 'png', parse_supported: true, parse_methods: ['disable', 'rapid_ocr'] });
|
||||
if (path === '/api/cloud-agents/files/save') return result({ saved: true });
|
||||
if (path !== '/api/cloud-agents/actions') throw new Error('Unexpected fixture path: ' + path);
|
||||
const operation = String(input?.operation);
|
||||
const args = input?.input as Record<string, unknown> | undefined;
|
||||
@@ -155,7 +174,8 @@ async function installFixture(page: Page, kind: FixtureKind) {
|
||||
if (!state.attachments.some(item => item.file_id === attachment.file_id)) state.attachments.push(attachment);
|
||||
return result({ attachments: [attachment] });
|
||||
}
|
||||
case 'files': return result({ files: [{ name: '故事笔记.md', is_dir: false, path: '/故事笔记.md', size: 1024 }] });
|
||||
case 'files': return result({ files: ['故事笔记.md', 'deliverable.py', 'internal.py', 'call.txt', 'cache.json', 'plain.txt', 'inline.py', 'example.py', 'user.txt', 'thought.txt']
|
||||
.map(name => ({ name, is_dir: false, path: '/' + name, directory_path: '/', size: 1024 })) });
|
||||
case 'run': return result(Object.values(state.histories).find(item => item.run?.agent_run_id === args?.run_id)?.run);
|
||||
case 'cancelRun': return result({ status: 'cancel_requested' });
|
||||
case 'resume': {
|
||||
@@ -175,12 +195,12 @@ async function installFixture(page: Page, kind: FixtureKind) {
|
||||
});
|
||||
}
|
||||
|
||||
async function openChat(page: Page, kind: FixtureKind, width: number, height: number) {
|
||||
async function openChat(page: Page, kind: FixtureKind, width: number, height: number, longName = false) {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.setViewportSize({ width, height });
|
||||
await installFixture(page, kind);
|
||||
await page.goto(baseUrl);
|
||||
await page.goto(baseUrl + (longName ? '?longName=1' : ''));
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('消息', { exact: true })).toBeEnabled();
|
||||
await expect(page.getByText('正在读取对话…', { exact: true })).toHaveCount(0);
|
||||
@@ -229,6 +249,46 @@ async function emitRunEvent(page: Page, type: 'messages' | 'end', id: string, en
|
||||
}, { type, id, envelope });
|
||||
}
|
||||
|
||||
test('long Agent identity and attachment entry fit on a short narrow window', async ({ page }) => {
|
||||
const errors = await openChat(page, 'history', 375, 540, true);
|
||||
const entry = page.getByRole('button', { name: '附件,共 2 个文件', exact: true });
|
||||
await expect(entry).toBeVisible();
|
||||
const header = page.locator('.agent-chat-workspace-header');
|
||||
expect((await header.boundingBox())!.height).toBeLessThanOrEqual(104);
|
||||
await expect(header.locator('.agent-chat-header-name')).toHaveAttribute('title', '我的故事搭档'.repeat(16));
|
||||
await expect(header.locator('.agent-attachment-entry')).toHaveCount(1);
|
||||
await expectComposerBounds(page);
|
||||
await entry.click();
|
||||
await expect(page.getByRole('dialog', { name: '本次对话的附件' })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(entry).toBeFocused();
|
||||
await expectComposerBounds(page);
|
||||
await screenshot(page, 'agent-chat-long-name-attachment-header');
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('the file Sheet counts only uploads and files explicitly delivered in the assistant answer', async ({ page }) => {
|
||||
const errors = await openChat(page, 'delivery', 375, 640);
|
||||
const entry = page.getByRole('button', { name: '附件,共 2 个文件', exact: true });
|
||||
await expect(entry).toBeVisible();
|
||||
await entry.click();
|
||||
const panel = page.getByRole('dialog', { name: '本次对话的附件' });
|
||||
const generated = panel.getByRole('region', { name: '助手生成的' });
|
||||
await expect(generated.getByRole('button', { name: /^保存 / })).toHaveCount(1);
|
||||
await expect(generated.getByRole('button', { name: '保存 deliverable.py', exact: true })).toBeVisible();
|
||||
for (const name of ['故事笔记.md', 'internal.py', 'call.txt', 'cache.json', 'plain.txt', 'inline.py', 'example.py', 'user.txt', 'thought.txt']) {
|
||||
await expect(generated.getByText(name, { exact: true })).toHaveCount(0);
|
||||
}
|
||||
await generated.getByRole('button', { name: '保存 deliverable.py', exact: true }).click();
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as FixtureWindow).__cloudChatFixture.calls
|
||||
.filter(call => call.path === '/api/cloud-agents/files/save').map(call => call.input))).toEqual([{ thread_id: 'story-first', path: '/deliverable.py' }]);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(entry).toBeFocused();
|
||||
await expectComposerBounds(page);
|
||||
expect((await operations(page)).some(item => ['submit', 'preview', 'resume', 'deleteAttachment'].includes(String(item?.operation)))).toBe(false);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
for (const width of [375, 768, 1024, 1440]) {
|
||||
test(`friendly empty conversation keeps the composer reachable at ${width}px`, async ({ page }) => {
|
||||
const errors = await openChat(page, 'empty', width, width === 375 ? 640 : 760);
|
||||
@@ -249,7 +309,7 @@ for (const width of [375, 768, 1024, 1440]) {
|
||||
});
|
||||
}
|
||||
|
||||
for (const viewport of [{ width: 375, height: 540 }, { width: 1440, height: 900 }]) {
|
||||
for (const viewport of [{ width: 375, height: 540 }, { width: 768, height: 760 }, { width: 1024, height: 760 }, { width: 1440, height: 900 }]) {
|
||||
test(`long Markdown history scrolls independently at ${viewport.width}×${viewport.height}`, async ({ page }) => {
|
||||
const errors = await openChat(page, 'history', viewport.width, viewport.height);
|
||||
const transcript = page.getByTestId('cloud-chat-messages');
|
||||
@@ -282,17 +342,34 @@ for (const viewport of [{ width: 375, height: 540 }, { width: 1440, height: 900
|
||||
await expect(page.getByLabel('历史对话', { exact: true })).toHaveCount(0);
|
||||
await expect(transcript.getByRole('article')).toHaveCount(16);
|
||||
await input.fill('');
|
||||
await page.getByRole('button', { name: '对话文件', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '添加文件或图片', exact: true })).toBeVisible();
|
||||
const entry = page.getByRole('button', { name: /^附件,共 2 个文件$/ });
|
||||
await expect(composer.getByRole('button', { name: /附件,共/ })).toHaveCount(0);
|
||||
await entry.click();
|
||||
const panel = page.getByRole('dialog', { name: '本次对话的附件' });
|
||||
await expect(panel).toBeVisible();
|
||||
const bounds = await panel.boundingBox();
|
||||
expect(bounds!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(viewport.width + 1);
|
||||
expect(Math.abs(bounds!.x + bounds!.width - viewport.width)).toBeLessThanOrEqual(1);
|
||||
expect(await panel.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await expect(page.getByRole('button', { name: '保存 故事笔记.md', exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: '保存 故事笔记.md', exact: true }).click();
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as FixtureWindow).__cloudChatFixture.calls
|
||||
.filter(call => call.path === '/api/cloud-agents/files/save').map(call => call.input))).toEqual([{ thread_id: 'story-first', path: '/故事笔记.md' }]);
|
||||
const chosen = page.getByRole('checkbox', { name: '选择附件 我的故事笔记与新发现.txt', exact: true });
|
||||
await chosen.check();
|
||||
await screenshot(page, `agent-chat-attachment-panel-${viewport.width}`);
|
||||
await page.getByRole('button', { name: /返回聊天/ }).click();
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(composer.getByRole('button', { name: '移除附件 我的故事笔记与新发现.txt', exact: true })).toBeVisible();
|
||||
await composer.getByRole('button', { name: '移除附件 我的故事笔记与新发现.txt', exact: true }).click();
|
||||
await entry.click();
|
||||
await expect(chosen).not.toBeChecked();
|
||||
await expect(composer.locator('.agent-attachment-selected')).toHaveCount(0);
|
||||
expect((await operations(page)).some(item => item?.operation === 'deleteAttachment')).toBe(false);
|
||||
await page.getByRole('button', { name: '对话文件', exact: true }).click();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(entry).toBeFocused();
|
||||
await expectComposerBounds(page);
|
||||
expect((await operations(page)).some(item => ['submit', 'preview', 'resume'].includes(String(item?.operation)))).toBe(false);
|
||||
expect(errors).toEqual([]);
|
||||
@@ -306,6 +383,7 @@ for (const width of [375, 1440]) {
|
||||
const send = composer.getByRole('button', { name: '发送', exact: true });
|
||||
await expect(send).toBeDisabled();
|
||||
await composer.getByRole('button', { name: '添加文件或图片', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: '添加文件或图片', exact: true }).click();
|
||||
const remove = composer.getByRole('button', { name: '移除附件 狐狸照片.png', exact: true });
|
||||
await expect(remove).toBeVisible();
|
||||
await expect(composer.locator('.agent-attachment-card[data-kind="image"]')).toHaveCount(1);
|
||||
@@ -320,8 +398,9 @@ for (const width of [375, 1440]) {
|
||||
await remove.click();
|
||||
await expect(composer.locator('.agent-attachment-selected')).toHaveCount(0);
|
||||
await expect(send).toBeDisabled();
|
||||
await composer.getByRole('button', { name: '对话文件', exact: true }).click();
|
||||
await expect(composer.getByRole('checkbox', { name: '选择附件 狐狸照片.png', exact: true })).not.toBeChecked();
|
||||
await page.getByRole('button', { name: /^附件,共 3 个文件$/ }).click();
|
||||
await expect(page.getByRole('checkbox', { name: '选择附件 狐狸照片.png', exact: true })).not.toBeChecked();
|
||||
await page.getByRole('button', { name: '关闭附件面板' }).click();
|
||||
expect((await operations(page)).some(item => ['deleteAttachment', 'submit', 'preview'].includes(String(item?.operation)))).toBe(false);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user