feat(agents): move conversation attachments into a header side panel
This commit is contained in:
1 parent
e9f204776a
commit
0d54690dd6
10 files changed
+589
-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>');
|
||||
});
|
||||
@@ -140,6 +150,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;
|
||||
@@ -175,12 +186,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 +240,24 @@ 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([]);
|
||||
});
|
||||
|
||||
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 +278,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 +311,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 +352,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 +367,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