fix(plugin-ai): scope attachment drop area to chat box (#10384)

This commit is contained in:
Katherine
2026-08-18 17:45:46 +08:00
committed by GitHub
parent 7d20016085
commit eb506fb498
4 changed files with 59 additions and 2 deletions
@@ -477,7 +477,7 @@ const UploadFiles: React.FC<{ disabled?: boolean }> = observer(({ disabled }) =>
return (
<Attachments
getDropContainer={() => chatBoxRef?.current ?? document.body}
getDropContainer={() => chatBoxRef?.current}
styles={{
placeholder: {
opacity: 0.8,
@@ -21,6 +21,9 @@ const mocks = vi.hoisted(() => {
nickname: 'Sales',
},
readonly: false,
chatBoxRef: {
current: null as HTMLDivElement | null,
},
},
chatConversationModel: {
currentConversation: undefined as string | undefined,
@@ -44,6 +47,7 @@ const mocks = vi.hoisted(() => {
return {
runtime,
attachmentsGetDropContainer: undefined as (() => HTMLElement | null | undefined) | undefined,
send: vi.fn(),
cancelRequest: vi.fn(),
finishEditingMessage: vi.fn(),
@@ -58,7 +62,16 @@ vi.mock('@ant-design/x', async () => {
MockButton.displayName = 'MockButton';
return {
Attachments: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
Attachments: ({
children,
getDropContainer,
}: {
children?: React.ReactNode;
getDropContainer?: () => HTMLElement | null | undefined;
}) => {
mocks.attachmentsGetDropContainer = getDropContainer;
return <div>{children}</div>;
},
Sender: React.forwardRef<
{ nativeElement?: HTMLTextAreaElement },
{
@@ -167,9 +180,24 @@ describe('Sender input state', () => {
mocks.runtime.chatSenderModel.setSenderValue.mockClear();
mocks.runtime.chatSenderModel.setSenderRef.mockClear();
mocks.runtime.chatSenderModel.setShowSenderHint.mockClear();
mocks.runtime.chatBoxModel.chatBoxRef.current = null;
mocks.attachmentsGetDropContainer = undefined;
mocks.send.mockClear();
});
it('limits attachment drops to the mounted chat box', () => {
const dropContainer = document.createElement('div');
mocks.runtime.chatBoxModel.chatBoxRef.current = dropContainer;
render(
<Sender showContextSelector={false} showWebSearch={false} showEmployeeSelect={false} showModelSelect={false} />,
);
expect(mocks.attachmentsGetDropContainer?.()).toBe(dropContainer);
mocks.runtime.chatBoxModel.chatBoxRef.current = null;
expect(mocks.attachmentsGetDropContainer?.()).toBeNull();
});
it('keeps typing local until blur', () => {
const { getByTestId } = render(
<Sender
@@ -23,6 +23,7 @@ const mocks = vi.hoisted(() => ({
getAIEmployees: vi.fn(),
refreshAITools: vi.fn(),
setRoles: vi.fn(),
setChatBoxRef: vi.fn(),
aiEmployees: [] as Array<{ username: string; nickname?: string; category?: string }>,
currentEmployee: { username: 'legacy', nickname: 'Legacy' },
currentConversation: undefined as string | undefined,
@@ -60,6 +61,7 @@ vi.mock('../../../ai-employees/chatbox/stores/runtime', () => ({
roles: {},
senderValue: mocks.senderValue,
setRoles: mocks.setRoles,
setChatBoxRef: mocks.setChatBoxRef,
setSenderValue: mocks.setSenderValue,
},
chatConversationModel: {
@@ -132,6 +134,23 @@ const createAIChatBoxCoreViewNode = (props: AIChatBoxBlockProps) => {
};
describe('AIChatBoxCoreView', () => {
it('registers its root as the attachment drop container', () => {
mocks.setChatBoxRef.mockReset();
mocks.currentConversation = undefined;
mocks.draftMessages = [];
mocks.getAIEmployees.mockResolvedValue([]);
const { container, unmount } = render(createAIChatBoxCoreViewNode({ showMessages: false, showDisclaimer: false }));
const root = container.firstElementChild;
const registeredRef = mocks.setChatBoxRef.mock.calls[0][0] as React.MutableRefObject<HTMLDivElement | null>;
expect(registeredRef.current).toBe(root);
expect(root).toHaveStyle({ position: 'relative' });
unmount();
expect(mocks.setChatBoxRef).toHaveBeenLastCalledWith(null);
});
it('keeps messages in a bounded flex region above the sender', () => {
mocks.messagesRendered = false;
mocks.currentConversation = undefined;
@@ -46,6 +46,7 @@ export const AIChatBoxCoreView: React.FC = observer(() => {
const hasDraftUserMessage = draftMessages.some((message) => message.role === 'user');
const { switchAIEmployee } = useChatBoxActions(runtime);
const chatBoxRef = useRef<HTMLDivElement | null>(null);
const defaultUserMessageStateRef = useRef<{
draftKey?: string;
value?: string;
@@ -54,6 +55,13 @@ export const AIChatBoxCoreView: React.FC = observer(() => {
useChatBoxEffect(runtime);
useEffect(() => {
chatBoxModel.setChatBoxRef(chatBoxRef);
return () => {
chatBoxModel.setChatBoxRef(null);
};
}, [chatBoxModel]);
useEffect(() => {
if (currentConversation) {
return;
@@ -140,12 +148,14 @@ export const AIChatBoxCoreView: React.FC = observer(() => {
return (
<div
ref={chatBoxRef}
style={{
height: '100%',
maxHeight: '100%',
minHeight: 0,
display: 'flex',
flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
backgroundColor: 'transparent',
}}