| | import React, { useRef, useState, useMemo } from 'react'; |
| | import { useRecoilState } from 'recoil'; |
| | import * as Ariakit from '@ariakit/react'; |
| | import { |
| | FileSearch, |
| | ImageUpIcon, |
| | FileType2Icon, |
| | FileImageIcon, |
| | TerminalSquareIcon, |
| | } from 'lucide-react'; |
| | import { |
| | EToolResources, |
| | EModelEndpoint, |
| | defaultAgentCapabilities, |
| | isDocumentSupportedProvider, |
| | } from 'librechat-data-provider'; |
| | import { |
| | FileUpload, |
| | TooltipAnchor, |
| | DropdownPopup, |
| | AttachmentIcon, |
| | SharePointIcon, |
| | } from '@librechat/client'; |
| | import type { EndpointFileConfig } from 'librechat-data-provider'; |
| | import { |
| | useAgentToolPermissions, |
| | useAgentCapabilities, |
| | useGetAgentsConfig, |
| | useFileHandling, |
| | useLocalize, |
| | } from '~/hooks'; |
| | import useSharePointFileHandling from '~/hooks/Files/useSharePointFileHandling'; |
| | import { SharePointPickerDialog } from '~/components/SharePoint'; |
| | import { useGetStartupConfig } from '~/data-provider'; |
| | import { ephemeralAgentByConvoId } from '~/store'; |
| | import { MenuItemProps } from '~/common'; |
| | import { cn } from '~/utils'; |
| |
|
| | interface AttachFileMenuProps { |
| | agentId?: string | null; |
| | endpoint?: string | null; |
| | disabled?: boolean | null; |
| | conversationId: string; |
| | endpointType?: EModelEndpoint; |
| | endpointFileConfig?: EndpointFileConfig; |
| | } |
| |
|
| | const AttachFileMenu = ({ |
| | agentId, |
| | endpoint, |
| | disabled, |
| | endpointType, |
| | conversationId, |
| | endpointFileConfig, |
| | }: AttachFileMenuProps) => { |
| | const localize = useLocalize(); |
| | const isUploadDisabled = disabled ?? false; |
| | const inputRef = useRef<HTMLInputElement>(null); |
| | const [isPopoverActive, setIsPopoverActive] = useState(false); |
| | const [ephemeralAgent, setEphemeralAgent] = useRecoilState( |
| | ephemeralAgentByConvoId(conversationId), |
| | ); |
| | const [toolResource, setToolResource] = useState<EToolResources | undefined>(); |
| | const { handleFileChange } = useFileHandling(); |
| | const { handleSharePointFiles, isProcessing, downloadProgress } = useSharePointFileHandling({ |
| | toolResource, |
| | }); |
| |
|
| | const { agentsConfig } = useGetAgentsConfig(); |
| | const { data: startupConfig } = useGetStartupConfig(); |
| | const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled; |
| |
|
| | const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false); |
| |
|
| | |
| | |
| | |
| | |
| | const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities); |
| |
|
| | const { fileSearchAllowedByAgent, codeAllowedByAgent, provider } = useAgentToolPermissions( |
| | agentId, |
| | ephemeralAgent, |
| | ); |
| |
|
| | const handleUploadClick = ( |
| | fileType?: 'image' | 'document' | 'multimodal' | 'google_multimodal', |
| | ) => { |
| | if (!inputRef.current) { |
| | return; |
| | } |
| | inputRef.current.value = ''; |
| | if (fileType === 'image') { |
| | inputRef.current.accept = 'image/*'; |
| | } else if (fileType === 'document') { |
| | inputRef.current.accept = '.pdf,application/pdf'; |
| | } else if (fileType === 'multimodal') { |
| | inputRef.current.accept = 'image/*,.pdf,application/pdf'; |
| | } else if (fileType === 'google_multimodal') { |
| | inputRef.current.accept = 'image/*,.pdf,application/pdf,video/*,audio/*'; |
| | } else { |
| | inputRef.current.accept = ''; |
| | } |
| | inputRef.current.click(); |
| | inputRef.current.accept = ''; |
| | }; |
| |
|
| | const dropdownItems = useMemo(() => { |
| | const createMenuItems = ( |
| | onAction: (fileType?: 'image' | 'document' | 'multimodal' | 'google_multimodal') => void, |
| | ) => { |
| | const items: MenuItemProps[] = []; |
| |
|
| | const currentProvider = provider || endpoint; |
| | if ( |
| | isDocumentSupportedProvider(endpointType) || |
| | isDocumentSupportedProvider(currentProvider) |
| | ) { |
| | items.push({ |
| | label: localize('com_ui_upload_provider'), |
| | onClick: () => { |
| | setToolResource(undefined); |
| | onAction( |
| | (provider || endpoint) === EModelEndpoint.google ? 'google_multimodal' : 'multimodal', |
| | ); |
| | }, |
| | icon: <FileImageIcon className="icon-md" />, |
| | }); |
| | } else { |
| | items.push({ |
| | label: localize('com_ui_upload_image_input'), |
| | onClick: () => { |
| | setToolResource(undefined); |
| | onAction('image'); |
| | }, |
| | icon: <ImageUpIcon className="icon-md" />, |
| | }); |
| | } |
| |
|
| | if (capabilities.contextEnabled) { |
| | items.push({ |
| | label: localize('com_ui_upload_ocr_text'), |
| | onClick: () => { |
| | setToolResource(EToolResources.context); |
| | onAction(); |
| | }, |
| | icon: <FileType2Icon className="icon-md" />, |
| | }); |
| | } |
| |
|
| | if (capabilities.fileSearchEnabled && fileSearchAllowedByAgent) { |
| | items.push({ |
| | label: localize('com_ui_upload_file_search'), |
| | onClick: () => { |
| | setToolResource(EToolResources.file_search); |
| | setEphemeralAgent((prev) => ({ |
| | ...prev, |
| | [EToolResources.file_search]: true, |
| | })); |
| | onAction(); |
| | }, |
| | icon: <FileSearch className="icon-md" />, |
| | }); |
| | } |
| |
|
| | if (capabilities.codeEnabled && codeAllowedByAgent) { |
| | items.push({ |
| | label: localize('com_ui_upload_code_files'), |
| | onClick: () => { |
| | setToolResource(EToolResources.execute_code); |
| | setEphemeralAgent((prev) => ({ |
| | ...prev, |
| | [EToolResources.execute_code]: true, |
| | })); |
| | onAction(); |
| | }, |
| | icon: <TerminalSquareIcon className="icon-md" />, |
| | }); |
| | } |
| |
|
| | return items; |
| | }; |
| |
|
| | const localItems = createMenuItems(handleUploadClick); |
| |
|
| | if (sharePointEnabled) { |
| | const sharePointItems = createMenuItems(() => { |
| | setIsSharePointDialogOpen(true); |
| | |
| | }); |
| | localItems.push({ |
| | label: localize('com_files_upload_sharepoint'), |
| | onClick: () => {}, |
| | icon: <SharePointIcon className="icon-md" />, |
| | subItems: sharePointItems, |
| | }); |
| | return localItems; |
| | } |
| |
|
| | return localItems; |
| | }, [ |
| | localize, |
| | endpoint, |
| | provider, |
| | endpointType, |
| | capabilities, |
| | setToolResource, |
| | setEphemeralAgent, |
| | sharePointEnabled, |
| | codeAllowedByAgent, |
| | fileSearchAllowedByAgent, |
| | setIsSharePointDialogOpen, |
| | ]); |
| |
|
| | const menuTrigger = ( |
| | <TooltipAnchor |
| | render={ |
| | <Ariakit.MenuButton |
| | disabled={isUploadDisabled} |
| | id="attach-file-menu-button" |
| | aria-label="Attach File Options" |
| | className={cn( |
| | 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50', |
| | )} |
| | > |
| | <div className="flex w-full items-center justify-center gap-2"> |
| | <AttachmentIcon /> |
| | </div> |
| | </Ariakit.MenuButton> |
| | } |
| | id="attach-file-menu-button" |
| | description={localize('com_sidepanel_attach_files')} |
| | disabled={isUploadDisabled} |
| | /> |
| | ); |
| | const handleSharePointFilesSelected = async (sharePointFiles: any[]) => { |
| | try { |
| | await handleSharePointFiles(sharePointFiles); |
| | setIsSharePointDialogOpen(false); |
| | } catch (error) { |
| | console.error('SharePoint file processing error:', error); |
| | } |
| | }; |
| |
|
| | return ( |
| | <> |
| | <FileUpload |
| | ref={inputRef} |
| | handleFileChange={(e) => { |
| | handleFileChange(e, toolResource); |
| | }} |
| | > |
| | <DropdownPopup |
| | menuId="attach-file-menu" |
| | className="overflow-visible" |
| | isOpen={isPopoverActive} |
| | setIsOpen={setIsPopoverActive} |
| | modal={true} |
| | unmountOnHide={true} |
| | trigger={menuTrigger} |
| | items={dropdownItems} |
| | iconClassName="mr-0" |
| | /> |
| | </FileUpload> |
| | <SharePointPickerDialog |
| | isOpen={isSharePointDialogOpen} |
| | onOpenChange={setIsSharePointDialogOpen} |
| | onFilesSelected={handleSharePointFilesSelected} |
| | isDownloading={isProcessing} |
| | downloadProgress={downloadProgress} |
| | maxSelectionCount={endpointFileConfig?.fileLimit} |
| | /> |
| | </> |
| | ); |
| | }; |
| |
|
| | export default React.memo(AttachFileMenu); |
| |
|