From 460bad395371ecf1e9c7bf901c20f9ea67c92a7f 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 11:22:10 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E8=BE=93=E5=85=A5=E6=A1=86=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8=E6=A0=B9=E6=8D=AE=E8=BE=93=E5=85=A5=E5=86=85=E5=AE=B9?= =?UTF-8?q?grow=EF=BC=8C=E8=BE=BE=E5=88=B0=E6=9C=80=E5=A4=A7height?= =?UTF-8?q?=E5=90=8E=E5=86=8D=E5=87=BA=E7=8E=B0=E7=BF=BB=E9=A1=B5=E6=9D=A1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../【编辑器】画布Agent对话面板-2026-07-03.md | 4 +- .../EditorAgentConversationPanelView.test.tsx | 127 ++++++++++++++---- .../EditorAgentConversationPanelView.tsx | 2 +- src/index.css | 25 ++++ 4 files changed, 129 insertions(+), 29 deletions(-) diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index e20029467..e83434286 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -59,8 +59,10 @@ - 对话框与既有任务侧栏(`ImageCanvasTaskSidebarView`)**互斥展开**:展开一个自动收起另一个;各自收起后保留入口按钮。 - 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。 - 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。 +- 底部消息输入框随输入内容从单行高度自动增长,最大高度为 + 128px;使用浏览器原生 `field-sizing: content` 随内容和可用宽度自适应,手机旋转、分屏或响应式断点变化后由浏览器按新的换行结果自动调整。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。 - 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。 -- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后切换到返回的空白会话并清空输入文字、附件及附件选择状态,旧会话继续保留在历史会话下拉中;创建失败时保留当前会话和未发送内容。 +- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后只切换到返回的空白会话,输入文字、附件及附件选择状态与切换历史会话时一样原样保留,旧会话继续保留在历史会话下拉中;创建失败同样不修改草稿。 - 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。 - 普通 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 03fdc0a8a..b1e7b00a7 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -11,6 +11,7 @@ import { import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { + EditorAgentConversationDetail, EditorAgentMessage, EditorAgentMessageResponse, } from '@/packages/shared/src/contracts'; @@ -269,12 +270,21 @@ describe('EditorAgentConversationPanelView', () => { }) as HTMLButtonElement; expect(newConversationButton.disabled).toBe(false); fireEvent.change(screen.getByLabelText('发送给画布 Agent'), { - target: { value: '不应带入新会话的草稿' }, + target: { value: '应保留到新会话的草稿' }, }); fireEvent.click(screen.getByRole('button', { name: '添加附件' })); - let attachmentDialog = screen.getByRole('dialog', { + const attachmentDialog = screen.getByRole('dialog', { name: '选择图片附件', }); + expect(attachmentDialog.parentElement?.className).toContain( + 'platform-theme--light', + ); + expect(attachmentDialog.className).toContain('platform-remap-surface'); + expect( + within(attachmentDialog).queryByRole('checkbox', { + name: '选择画布图片 未入库图层', + }), + ).toBeNull(); fireEvent.click( within(attachmentDialog).getByRole('checkbox', { name: '选择画布图片 角色图层', @@ -297,8 +307,8 @@ describe('EditorAgentConversationPanelView', () => { }); expect( (screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value, - ).toBe(''); - expect(screen.queryByText('角色图层')).toBeNull(); + ).toBe('应保留到新会话的草稿'); + expect(screen.getByText('角色图层')).toBeTruthy(); expect( screen.getByRole('option', { name: '角色参考' }), ).toBeTruthy(); @@ -311,29 +321,6 @@ describe('EditorAgentConversationPanelView', () => { fireEvent.click(newConversationButton); expect(client.createConversation).toHaveBeenCalledTimes(1); - fireEvent.click(screen.getByRole('button', { name: '添加附件' })); - attachmentDialog = screen.getByRole('dialog', { - name: '选择图片附件', - }); - expect(attachmentDialog.parentElement?.className).toContain( - 'platform-theme--light', - ); - expect(attachmentDialog.className).toContain('platform-remap-surface'); - expect( - within(attachmentDialog).queryByRole('checkbox', { - name: '选择画布图片 未入库图层', - }), - ).toBeNull(); - fireEvent.click( - within(attachmentDialog).getByRole('checkbox', { - name: '选择画布图片 角色图层', - }), - ); - fireEvent.click( - within(attachmentDialog).getByRole('button', { name: '应用' }), - ); - expect(screen.getByText('角色图层')).toBeTruthy(); - fireEvent.change(screen.getByLabelText('发送给画布 Agent'), { target: { value: '参考附件做像素风' }, }); @@ -420,6 +407,64 @@ describe('EditorAgentConversationPanelView', () => { ).toBe('conversation-1'); }); + it('preserves newer draft edits while creating a conversation', async () => { + const client = createClient(); + let resolveCreate!: (detail: EditorAgentConversationDetail) => void; + vi.mocked(client.createConversation).mockImplementationOnce( + () => + new Promise((resolve) => { + resolveCreate = resolve; + }), + ); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByText('已经看到画布内容')).toBeTruthy(); + }); + const draftInput = screen.getByLabelText( + '发送给画布 Agent', + ) as HTMLTextAreaElement; + fireEvent.change(draftInput, { + target: { value: '旧项目草稿' }, + }); + fireEvent.click(screen.getByRole('button', { name: '新建对话' })); + + await waitFor(() => { + expect(client.createConversation).toHaveBeenCalledWith('project-1', {}); + expect(draftInput.disabled).toBe(false); + }); + expect( + (screen.getByRole('button', { name: '添加附件' }) as HTMLButtonElement) + .disabled, + ).toBe(false); + fireEvent.change(draftInput, { + target: { value: '创建期间更新的草稿' }, + }); + + await act(async () => { + resolveCreate({ + conversationId: 'conversation-2', + projectId: 'project-1', + title: '新对话', + messages: [], + createdAt: '2026-07-03T00:01:00.000Z', + updatedAt: '2026-07-03T00:01:00.000Z', + }); + await Promise.resolve(); + }); + expect(draftInput.value).toBe('创建期间更新的草稿'); + expect( + (screen.getByLabelText('当前对话') as HTMLSelectElement).value, + ).toBe('conversation-2'); + }); + it('disables sending while a message request is pending without showing stop', async () => { const client = createClient(); let resolveSend!: (response: EditorAgentMessageResponse) => void; @@ -927,6 +972,34 @@ describe('EditorAgentConversationPanelView', () => { expect(inputWheel.defaultPrevented).toBe(false); }); + it('uses native content sizing with minimum and maximum height constraints', async () => { + const client = createClient(); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByText('已经看到画布内容')).toBeTruthy(); + }); + + const input = screen.getByLabelText( + '发送给画布 Agent', + ) as HTMLTextAreaElement; + expect(input.className).toContain( + 'editor-agent-conversation__draft-input', + ); + expect(input.className).toContain('[field-sizing:content]'); + expect(input.className).toContain('min-h-10'); + expect(input.className).toContain('max-h-32'); + expect(input.className).toContain('overflow-y-auto'); + expect(input.className).not.toContain('overflow-y-hidden'); + }); + it('confirms conversation deletion from an independent dialog', async () => { const client = createClient(); render( diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx index d047a8965..155589f32 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx @@ -705,7 +705,7 @@ export function EditorAgentConversationPanelView({