LibreChat/client/src/components/SidePanel/Agents/FileContext.tsx
Danny Avila cde5079886
🎯 fix: Use Agents Endpoint Config for Agent Panel File Upload Validation (#11992)
* fix: Use correct endpoint for file validation in agent panel uploads

  Agent panel file uploads (FileSearch, FileContext, Code/Files) were validating against the active conversation's endpoint config instead of the agents endpoint config. This caused incorrect file size limits when the active chat used a different endpoint.

  Add endpointOverride option to useFileHandling so callers can specify the correct endpoint for validation independent of the active conversation.

* fix: Use agents endpoint config for agent panel file upload validation

Agent panel file uploads (FileSearch, FileContext, Code/Files) validated
against the active conversation's endpoint config instead of the agents
endpoint config. This caused wrong file size limits when the active chat
used a different endpoint.

Adds endpointOverride to useFileHandling so callers can specify the
correct endpoint for both validation and upload routing, independent of
the active conversation.

* test: Add unit tests for useFileHandling hook to validate endpoint overrides

Introduced comprehensive tests for the useFileHandling hook, ensuring correct behavior when using endpoint overrides for file validation and upload routing. The tests cover various scenarios, including fallback to conversation endpoints and proper handling of agent-specific configurations, enhancing the reliability of file handling in the application.
2026-02-28 15:01:51 -05:00

201 lines
6.6 KiB
TypeScript

import { useState, useRef } from 'react';
import { Folder } from 'lucide-react';
import * as Ariakit from '@ariakit/react';
import {
EModelEndpoint,
EToolResources,
mergeFileConfig,
getEndpointFileConfig,
} from 'librechat-data-provider';
import {
HoverCard,
DropdownPopup,
AttachmentIcon,
CircleHelpIcon,
SharePointIcon,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '@librechat/client';
import type { ExtendedFile } from '~/common';
import { useFileHandling, useLocalize, useLazyEffect, useSharePointFileHandling } from '~/hooks';
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
import { SharePointPickerDialog } from '~/components/SharePoint';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { ESide, isEphemeralAgent } from '~/common';
import { useChatContext } from '~/Providers';
export default function FileContext({
agent_id,
files: _files,
}: {
agent_id: string;
files?: [string, ExtendedFile][];
}) {
const localize = useLocalize();
const { setFilesLoading } = useChatContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
const [isPopoverActive, setIsPopoverActive] = useState(false);
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
const { data: startupConfig } = useGetStartupConfig();
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
const { data: fileConfig = null } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
additionalMetadata: { agent_id, tool_resource: EToolResources.context },
endpointOverride: EModelEndpoint.agents,
fileSetter: setFiles,
});
const { handleSharePointFiles, isProcessing, downloadProgress } = useSharePointFileHandling({
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
endpointOverride: EModelEndpoint.agents,
fileSetter: setFiles,
});
useLazyEffect(
() => {
if (_files) {
setFiles(new Map(_files));
}
},
[_files],
750,
);
const endpointFileConfig = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.agents,
endpointType: EModelEndpoint.agents,
});
const isUploadDisabled = endpointFileConfig?.disabled ?? false;
const handleSharePointFilesSelected = async (sharePointFiles: any[]) => {
try {
await handleSharePointFiles(sharePointFiles);
setIsSharePointDialogOpen(false);
} catch (error) {
console.error('SharePoint file processing error:', error);
}
};
if (isUploadDisabled) {
return null;
}
const handleLocalFileClick = () => {
// necessary to reset the input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
fileInputRef.current?.click();
};
const dropdownItems = [
{
label: localize('com_files_upload_local_machine'),
onClick: handleLocalFileClick,
icon: <Folder className="icon-md" />,
},
{
label: localize('com_files_upload_sharepoint'),
onClick: () => setIsSharePointDialogOpen(true),
icon: <SharePointIcon className="icon-md" />,
},
];
const menuTrigger = (
<Ariakit.MenuButton className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium">
<div className="flex w-full items-center justify-center gap-1">
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
{localize('com_ui_upload_file_context')}
</div>
</Ariakit.MenuButton>
);
return (
<div className="w-full">
<HoverCard openDelay={50}>
<div className="mb-2 flex items-center gap-2">
<HoverCardTrigger asChild>
<span className="flex items-center gap-2">
<label className="text-token-text-primary block font-medium">
{localize('com_agents_file_context_label')}
</label>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</span>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_file_context_description')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
<div className="flex flex-col gap-3">
{/* File Context Files */}
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
agent_id={agent_id}
tool_resource={EToolResources.context}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
<div>
{sharePointEnabled ? (
<>
<DropdownPopup
gutter={2}
menuId="file-search-upload-menu"
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={menuTrigger}
items={dropdownItems}
modal={true}
unmountOnHide={true}
/>
</>
) : (
<button
type="button"
disabled={isEphemeralAgent(agent_id)}
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleLocalFileClick}
>
<div className="flex w-full items-center justify-center gap-1">
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
{localize('com_ui_upload_file_context')}
</div>
</button>
)}
<input
multiple={true}
type="file"
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={isEphemeralAgent(agent_id)}
onChange={handleFileChange}
/>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-xs text-text-secondary">
{localize('com_agents_file_context_disabled')}
</div>
)}
</div>
<SharePointPickerDialog
isOpen={isSharePointDialogOpen}
onOpenChange={setIsSharePointDialogOpen}
onFilesSelected={handleSharePointFilesSelected}
isDownloading={isProcessing}
downloadProgress={downloadProgress}
maxSelectionCount={endpointFileConfig?.fileLimit}
/>
</div>
);
}