fix interactive error
This commit is contained in:
@@ -116,6 +116,7 @@
|
||||
6. 错误气泡(失败/余额不足,带原因);
|
||||
7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,避免后端已持久化消息但前端中断请求后产生会话状态错位。
|
||||
8. 桌面端右键消息正文可复制该条可见文本;右键消息附件或生成结果可下载素材,图片额外支持复制图片本体和“引用”到当前输入区。引用复用附件去重、9 张上限和发送链路;右键菜单只保留已有的 `objectKey` 与素材 `source`,引用时由输入区优先按 `objectKey`、缺失时按 `source` 从当前画布和素材库选项重新查询并构造 `EditorAgentAttachmentRef`,同时命中时画布优先。
|
||||
9. 消息右键菜单遵循 Canva 式单实例交互:任一菜单已打开时,下一次右键必须先关闭旧菜单;新落点是消息正文或素材时再在新位置打开对应菜单,新落点没有右键动作时仅收起旧菜单,不允许多个消息菜单并存。复制、引用或下载成功后自动关闭菜单;失败时保留菜单和失败状态,避免错误无提示消失。
|
||||
|
||||
不做(明确排除,防止后人补齐):
|
||||
|
||||
|
||||
@@ -95,9 +95,74 @@ describe('MessageBubble', () => {
|
||||
await waitFor(() =>
|
||||
expect(copyTextToClipboard).toHaveBeenCalledWith('第一行\n第二行'),
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('menuitem', { name: '已复制' }),
|
||||
).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '消息右键菜单' }),
|
||||
).toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it('closes the previous menu before another right click opens a new one', () => {
|
||||
render(
|
||||
<>
|
||||
<MessageBubble
|
||||
message={{
|
||||
id: 40,
|
||||
role: 'assistant',
|
||||
text: '第一条消息',
|
||||
attachments: [],
|
||||
toolCall: null,
|
||||
createdAt: '2026-07-20T00:00:00Z',
|
||||
}}
|
||||
busyAction={null}
|
||||
onConfirmToolCall={vi.fn()}
|
||||
onCancelToolCall={vi.fn()}
|
||||
/>
|
||||
<MessageBubble
|
||||
message={{
|
||||
id: 41,
|
||||
role: 'assistant',
|
||||
text: '第二条消息',
|
||||
attachments: [],
|
||||
toolCall: null,
|
||||
createdAt: '2026-07-20T00:00:00Z',
|
||||
}}
|
||||
busyAction={null}
|
||||
onConfirmToolCall={vi.fn()}
|
||||
onCancelToolCall={vi.fn()}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
const messages = screen.getAllByLabelText('Agent消息');
|
||||
fireEvent.contextMenu(messages[0]!, { clientX: 30, clientY: 40 });
|
||||
fireEvent.contextMenu(messages[1]!, { clientX: 70, clientY: 80 });
|
||||
|
||||
const menus = screen.getAllByRole('menu', { name: '消息右键菜单' });
|
||||
expect(menus).toHaveLength(1);
|
||||
expect(menus[0]?.style.left).toBe('70px');
|
||||
expect(menus[0]?.style.top).toBe('80px');
|
||||
});
|
||||
|
||||
it('closes an open menu when the next right click has no menu target', () => {
|
||||
renderMessage({
|
||||
id: 42,
|
||||
role: 'assistant',
|
||||
text: '右键后应可收起',
|
||||
attachments: [],
|
||||
toolCall: null,
|
||||
createdAt: '2026-07-20T00:00:00Z',
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(screen.getByLabelText('Agent消息'), {
|
||||
clientX: 30,
|
||||
clientY: 40,
|
||||
});
|
||||
expect(screen.getByRole('menu', { name: '消息右键菜单' })).toBeTruthy();
|
||||
|
||||
fireEvent.contextMenu(document.body, { clientX: 70, clientY: 80 });
|
||||
|
||||
expect(screen.queryByRole('menu', { name: '消息右键菜单' })).toBeNull();
|
||||
});
|
||||
|
||||
it('copies an attached WebP as PNG without opening the message text menu', async () => {
|
||||
@@ -182,7 +247,11 @@ describe('MessageBubble', () => {
|
||||
.fn()
|
||||
.mockResolvedValue(new Blob(['webp'], { type: 'image/webp' })),
|
||||
} as unknown as Response);
|
||||
await screen.findByRole('menuitem', { name: '已复制' });
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
|
||||
).toBeNull(),
|
||||
);
|
||||
expect(bitmapClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -219,9 +288,11 @@ describe('MessageBubble', () => {
|
||||
objectKey: 'editor/reference.png',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('menuitem', { name: '已引用' }),
|
||||
).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
|
||||
).toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it('offers reference for generated images using their media lookup fields', async () => {
|
||||
@@ -335,6 +406,9 @@ describe('MessageBubble', () => {
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '下载视频' }));
|
||||
|
||||
await waitFor(() => expect(anchorClick).toHaveBeenCalledTimes(1));
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
|
||||
).toBeNull();
|
||||
expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob));
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith('blob:agent-video');
|
||||
});
|
||||
|
||||
@@ -115,17 +115,25 @@ export function MessageBubbleRightClickMenu({
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
if (menuRef.current?.contains(event.target as Node)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('pointerdown', handlePointerDown);
|
||||
window.addEventListener('contextmenu', handleContextMenu, true);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('scroll', onClose, true);
|
||||
window.addEventListener('resize', onClose);
|
||||
return () => {
|
||||
window.removeEventListener('pointerdown', handlePointerDown);
|
||||
window.removeEventListener('contextmenu', handleContextMenu, true);
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('scroll', onClose, true);
|
||||
window.removeEventListener('resize', onClose);
|
||||
|
||||
@@ -197,12 +197,14 @@ export function useRightClickMenu({
|
||||
: false;
|
||||
setRightClickMenu((current) =>
|
||||
current?.target === target && current.pendingAction === action
|
||||
? {
|
||||
...current,
|
||||
pendingAction: null,
|
||||
resultAction: action,
|
||||
result: succeeded ? 'success' : 'error',
|
||||
}
|
||||
? succeeded
|
||||
? null
|
||||
: {
|
||||
...current,
|
||||
pendingAction: null,
|
||||
resultAction: action,
|
||||
result: 'error',
|
||||
}
|
||||
: current,
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user