feat(agents): move conversation attachments into a header side panel

This commit is contained in:
鲨鱼辣椒 committed 2026-10-12 23:12:24 +08:00
1 parent e9f204776a
commit 0d54690dd6
10 files changed
+589 -98

No files matched your search

+62 -14
View File
@@ -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([]);
});