mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-28 17:43:07 +08:00
fix(plugin-ai): scope attachment drop area to chat box (#10384)
This commit is contained in:
+1
-1
@@ -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,
|
||||
|
||||
+29
-1
@@ -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
|
||||
|
||||
+19
@@ -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;
|
||||
|
||||
+10
@@ -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',
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user