From 9122d50d7102e80232c1532bac5f666f078c71ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 22 Jul 2026 10:44:38 +0800 Subject: [PATCH 01/12] =?UTF-8?q?fix:=20=E6=96=B0=E5=BB=BA=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=8C=89=E9=92=AE=E4=BA=A4=E4=BA=92=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=A9=BA=E5=AF=B9=E8=AF=9D=E6=97=B6?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=BD=AE=E7=81=B0=E4=B8=8D=E5=8F=AF=E7=82=B9?= =?UTF-8?q?=E5=87=BB=EF=BC=8C=E4=BF=9D=E7=95=99=E5=88=9B=E5=BB=BA=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=97=B6=E7=9A=84=E8=8D=89=E7=A8=BF=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../【编辑器】画布Agent对话面板-2026-07-03.md | 1 + .../EditorAgentConversationPanelView.test.tsx | 109 +++++++++++++++++- .../EditorAgentConversationPanelView.tsx | 23 +++- 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 63749082d..e20029467 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -60,6 +60,7 @@ - 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。 - 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。 - 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。 +- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后切换到返回的空白会话并清空输入文字、附件及附件选择状态,旧会话继续保留在历史会话下拉中;创建失败时保留当前会话和未发送内容。 - 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。 - 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。 - 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。 diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index 3072fa16c..03fdc0a8a 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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; diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx index 073da2c3b..80ae0a832 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx @@ -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} >