fix: 新建对话按钮交互优化,确保空对话时按钮置灰不可点击,保留创建失败时的草稿状态
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
- 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。
|
||||
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
|
||||
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
|
||||
- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后切换到返回的空白会话并清空输入文字、附件及附件选择状态,旧会话继续保留在历史会话下拉中;创建失败时保留当前会话和未发送内容。
|
||||
- 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。
|
||||
- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。
|
||||
- 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。
|
||||
|
||||
+107
-2
@@ -264,13 +264,55 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
const newConversationButton = screen.getByRole('button', {
|
||||
name: '新建对话',
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(false);
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '不应带入新会话的草稿' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
let attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('checkbox', {
|
||||
name: '选择画布图片 角色图层',
|
||||
}),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('button', { name: '应用' }),
|
||||
);
|
||||
expect(screen.getByText('角色图层')).toBeTruthy();
|
||||
|
||||
fireEvent.click(newConversationButton);
|
||||
await waitFor(() => {
|
||||
expect(client.createConversation).toHaveBeenCalledWith('project-1', {});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
(screen.getByLabelText('当前对话') as HTMLSelectElement).value,
|
||||
).toBe('conversation-2');
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('');
|
||||
expect(screen.queryByText('角色图层')).toBeNull();
|
||||
expect(
|
||||
screen.getByRole('option', { name: '角色参考' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '新对话' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '尚未发送的草稿' },
|
||||
});
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
const attachmentDialog = screen.getByRole('dialog', {
|
||||
attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
});
|
||||
expect(attachmentDialog.parentElement?.className).toContain(
|
||||
@@ -315,6 +357,69 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps new conversation disabled when the project has no conversation history', async () => {
|
||||
const client = createClient();
|
||||
vi.mocked(client.listConversations).mockResolvedValueOnce([]);
|
||||
|
||||
render(
|
||||
<EditorAgentConversationPanelView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
client={client}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(client.listConversations).toHaveBeenCalledWith('project-1');
|
||||
expect(screen.getByText('暂无消息')).toBeTruthy();
|
||||
});
|
||||
const newConversationButton = screen.getByRole('button', {
|
||||
name: '新建对话',
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '未发送内容' },
|
||||
});
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves the current conversation draft when creating a conversation fails', async () => {
|
||||
const client = createClient();
|
||||
vi.mocked(client.createConversation).mockRejectedValueOnce(
|
||||
new Error('创建新会话失败'),
|
||||
);
|
||||
|
||||
render(
|
||||
<EditorAgentConversationPanelView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
client={client}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '需要保留的草稿' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('创建新会话失败')).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('需要保留的草稿');
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('当前对话') as HTMLSelectElement).value,
|
||||
).toBe('conversation-1');
|
||||
});
|
||||
|
||||
it('disables sending while a message request is pending without showing stop', async () => {
|
||||
const client = createClient();
|
||||
let resolveSend!: (response: EditorAgentMessageResponse) => void;
|
||||
|
||||
+21
-2
@@ -307,6 +307,19 @@ export function EditorAgentConversationPanelView({
|
||||
|
||||
const hasProject = Boolean(projectId?.trim());
|
||||
const isConversationBusy = isWaiting || isToolCallActionPending;
|
||||
const hasCurrentConversationContent = messages.length > 0;
|
||||
|
||||
const handleCreateConversation = () => {
|
||||
void createConversation()
|
||||
.then(() => {
|
||||
setDraftText('');
|
||||
setAttachments([]);
|
||||
setAttachmentError(null);
|
||||
setDraftAttachmentKeys(new Set());
|
||||
setAttachmentPickerOpen(false);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
const openAttachmentPicker = () => {
|
||||
setAttachmentError(null);
|
||||
@@ -546,9 +559,15 @@ export function EditorAgentConversationPanelView({
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-900 text-white disabled:opacity-45"
|
||||
aria-label="新建对话"
|
||||
disabled={
|
||||
!hasProject || isCreatingConversation || isConversationBusy
|
||||
!hasProject ||
|
||||
!hasCurrentConversationContent ||
|
||||
isLoadingConversations ||
|
||||
isLoadingMessages ||
|
||||
isCreatingConversation ||
|
||||
isConversationBusy ||
|
||||
isPastingAttachment
|
||||
}
|
||||
onClick={() => void createConversation()}
|
||||
onClick={handleCreateConversation}
|
||||
>
|
||||
<Plus className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user