style: 输入框尺寸根据输入内容grow,达到最大height后再出现翻页条。

This commit is contained in:
2026-07-22 11:22:10 +08:00
parent aea43cfc10
commit 460bad3953
4 changed files with 129 additions and 29 deletions
@@ -59,8 +59,10 @@
- 对话框与既有任务侧栏(`ImageCanvasTaskSidebarView`)**互斥展开**:展开一个自动收起另一个;各自收起后保留入口按钮。
- 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
- 底部消息输入框随输入内容从单行高度自动增长,最大高度为
128px;使用浏览器原生 `field-sizing: content` 随内容和可用宽度自适应,手机旋转、分屏或响应式断点变化后由浏览器按新的换行结果自动调整。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后切换到返回的空白会话并清空输入文字、附件及附件选择状态,旧会话继续保留在历史会话下拉中;创建失败时保留当前会话和未发送内容
- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后切换到返回的空白会话输入文字、附件及附件选择状态与切换历史会话时一样原样保留,旧会话继续保留在历史会话下拉中;创建失败同样不修改草稿
- 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。
- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。
- 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。
@@ -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<EditorAgentConversationDetail>((resolve) => {
resolveCreate = resolve;
}),
);
render(
<EditorAgentConversationPanelView
open
onToggleOpen={vi.fn()}
client={client}
/>,
);
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(
<EditorAgentConversationPanelView
open
onToggleOpen={vi.fn()}
client={client}
/>,
);
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(
@@ -705,7 +705,7 @@ export function EditorAgentConversationPanelView({
<Paperclip className="h-4 w-4" aria-hidden="true" />
</button>
<textarea
className="max-h-32 min-h-10 flex-1 resize-none overflow-y-auto overscroll-contain rounded-3xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800 outline-none focus:border-slate-400"
className="editor-agent-conversation__draft-input max-h-32 min-h-10 min-w-0 flex-1 resize-none overflow-x-hidden overflow-y-auto overscroll-contain rounded-3xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800 outline-none [field-sizing:content] focus:border-slate-400"
aria-label="发送给画布 Agent"
value={draftText}
rows={1}
+25
View File
@@ -16694,6 +16694,31 @@ button {
background: #3f3f46;
}
.editor-agent-conversation__draft-input {
scrollbar-color: #cbd5e1 transparent;
scrollbar-width: thin;
}
.editor-agent-conversation__draft-input::-webkit-scrollbar {
width: 6px;
}
.editor-agent-conversation__draft-input::-webkit-scrollbar-track {
margin-block: 10px;
background-color: transparent;
}
.editor-agent-conversation__draft-input::-webkit-scrollbar-thumb {
border: 1px solid transparent;
border-radius: 999px;
background-color: #cbd5e1;
background-clip: padding-box;
}
.editor-agent-conversation__draft-input::-webkit-scrollbar-thumb:hover {
background-color: #94a3b8;
}
.image-canvas-editor__metadata-dialog.platform-modal-shell {
border-color: rgba(226, 232, 240, 0.92) !important;
background: #ffffff !important;