mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-08 09:02:36 +01:00
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* chore: Remove unused setValueOnChange prop from MCPServerMenuItem component
* fix: Resolve agent provider endpoint type for file upload support
When using the agents endpoint with a custom provider (e.g., Moonshot),
the endpointType was resolving to "agents" instead of the provider's
actual type ("custom"), causing "Upload to Provider" to not appear in
the file attach menu.
Adds `resolveEndpointType` utility in data-provider that follows the
chain: endpoint (if not agents) → agent.provider → agents. Applied
consistently across AttachFileChat, DragDropContext, useDragHelpers,
and AgentPanel file components (FileContext, FileSearch, Code/Files).
* refactor: Extract useAgentFileConfig hook, restore deleted tests, fix review findings
- Extract shared provider resolution logic into useAgentFileConfig hook
(Finding #2: DRY violation across FileContext, FileSearch, Code/Files)
- Restore 18 deleted test cases in AttachFileMenu.spec.tsx covering
agent capabilities, SharePoint, edge cases, and button state
(Finding #1: accidental test deletion)
- Wrap fileConfigEndpoint in useMemo in AttachFileChat (Finding #3)
- Fix misleading test name in AgentFileConfig.spec.tsx (Finding #4)
- Fix import order in FileSearch.tsx, FileContext.tsx, Code/Files.tsx (Finding #5)
- Add comment about cache gap in useDragHelpers (Finding #6)
- Clarify resolveEndpointType JSDoc (Finding #7)
* refactor: Memoize Footer component for performance optimization
- Converted Footer component to a memoized version to prevent unnecessary re-renders.
- Improved import structure by adding memo to the React import statement for clarity.
* chore: Fix remaining review nits
- Widen useAgentFileConfig return type to EModelEndpoint | string
- Fix import order in FileContext.tsx and FileSearch.tsx
- Remove dead endpointType param from setupMocks in AttachFileMenu test
* fix: Pass resolved provider endpoint to file upload validation
AgentPanel file components (FileContext, FileSearch, Code/Files) were
hardcoding endpointOverride to "agents", causing both client-side
validation (file limits, MIME types) and server-side validation to
use the agents config instead of the provider-specific config.
Adds endpointTypeOverride to UseFileHandling params so endpoint and
endpointType can be set independently. Components now pass the
resolved provider name and type from useAgentFileConfig, so the full
fallback chain (provider → custom → agents → default) applies to
file upload validation on both client and server.
* test: Verify any custom endpoint is document-supported regardless of name
Adds parameterized tests with arbitrary endpoint names (spaces, hyphens,
colons, etc.) confirming that all custom endpoints resolve to
document-supported through resolveEndpointType, both as direct
endpoints and as agent providers.
* fix: Use || for provider fallback, test endpointOverride wiring
- Change providerValue ?? to providerValue || so empty string is
treated as "no provider" consistently with resolveEndpointType
- Add wiring tests to CodeFiles, FileContext, FileSearch verifying
endpointOverride and endpointTypeOverride are passed correctly
- Update endpointOverride JSDoc to document endpointType fallback
202 lines
6.8 KiB
TypeScript
202 lines
6.8 KiB
TypeScript
import { memo, useMemo, useRef, useState } from 'react';
|
|
import { Folder } from 'lucide-react';
|
|
import * as Ariakit from '@ariakit/react';
|
|
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
|
|
import {
|
|
HoverCard,
|
|
DropdownPopup,
|
|
AttachmentIcon,
|
|
CircleHelpIcon,
|
|
SharePointIcon,
|
|
HoverCardPortal,
|
|
HoverCardContent,
|
|
HoverCardTrigger,
|
|
} from '@librechat/client';
|
|
import type { ExtendedFile } from '~/common';
|
|
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
|
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
|
import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks';
|
|
import { SharePointPickerDialog } from '~/components/SharePoint';
|
|
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
|
import { useGetStartupConfig } from '~/data-provider';
|
|
import { ESide, isEphemeralAgent } from '~/common';
|
|
|
|
function FileContext({
|
|
agent_id,
|
|
files: _files,
|
|
}: {
|
|
agent_id: string;
|
|
files?: [string, ExtendedFile][];
|
|
}) {
|
|
const localize = useLocalize();
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
|
const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]);
|
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
|
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
|
|
const { data: startupConfig } = useGetStartupConfig();
|
|
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
|
|
const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig();
|
|
const endpointOverride = providerValue || EModelEndpoint.agents;
|
|
|
|
const { handleFileChange } = useFileHandlingNoChatContext(
|
|
{
|
|
additionalMetadata: { agent_id, tool_resource: EToolResources.context },
|
|
endpointOverride,
|
|
endpointTypeOverride: endpointType,
|
|
fileSetter: setFiles,
|
|
},
|
|
fileHandlingState,
|
|
);
|
|
const { handleSharePointFiles, isProcessing, downloadProgress } =
|
|
useSharePointFileHandlingNoChatContext(
|
|
{
|
|
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
|
endpointOverride,
|
|
endpointTypeOverride: endpointType,
|
|
fileSetter: setFiles,
|
|
},
|
|
fileHandlingState,
|
|
);
|
|
useLazyEffect(
|
|
() => {
|
|
if (_files) {
|
|
setFiles(new Map(_files));
|
|
}
|
|
},
|
|
[_files],
|
|
750,
|
|
);
|
|
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}
|
|
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>
|
|
);
|
|
}
|
|
|
|
const MemoizedFileContext = memo(FileContext);
|
|
MemoizedFileContext.displayName = 'FileContext';
|
|
|
|
export default MemoizedFileContext;
|