mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 12:44:28 +00:00
📋 fix: Route Clipboard Paste Through Upload Options (#13957)
* 🐛 fix: route clipboard paste through upload-option guards Pasting a file skipped the composer's attachment guards, so unsupported types such as csv and xlsx reached the provider as document blocks and were rejected. Paste, drag, and the upload modal now share getViableUploadOptions to decide routing: zero viable destinations shows a toast, one auto-routes, several open the upload-type modal. * 🐛 fix: key ephemeral agent state by NEW_CONVO in upload-option flow useFileUploadRouter writes ephemeral capability state under `conversationId ?? Constants.NEW_CONVO`, but useUploadOptions and DragDropModal read it under `?? ''`, so on a new conversation the option resolver missed capabilities enabled by auto-routing. Align the reads on Constants.NEW_CONVO. * 🐛 fix: harden paste upload routing for assistants, custom endpoints, and toasts Bypass option resolution for Assistants endpoints on paste, matching drag-and-drop, so non-image assistant uploads use the assistants upload path instead of mis-routing to context or the unsupported toast. Honor a custom endpoint's configured supportedMimeTypes for direct provider attach instead of hardcoding image and PDF. Stop asserting upload success before validation runs; the single-route notice is now an informational "Attached as text" for the text-extraction case only. * 🐛 fix: refine paste upload routing for direct chats, custom endpoints, and disabled uploads Restore Code Interpreter and File Search options in direct and ephemeral chats by defaulting their permissions to allowed unless a saved agent omits the tool; selecting one still enables the ephemeral capability. Treat a custom endpoint as broad provider support only when its file config is permissive (matching the file picker), so an inherited default no longer offers zip/audio/video for direct attach. Short-circuit paste with the disabled-upload error before resolving options or opening the modal.
This commit is contained in:
parent
a0529c9af7
commit
edeb1ecc2c
12 changed files with 583 additions and 248 deletions
43
client/src/Providers/UploadModalContext.tsx
Normal file
43
client/src/Providers/UploadModalContext.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
|
||||
interface UploadModalContextValue {
|
||||
isVisible: boolean;
|
||||
files: File[];
|
||||
openModal: (files: File[]) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const defaultValue: UploadModalContextValue = {
|
||||
isVisible: false,
|
||||
files: [],
|
||||
openModal: () => undefined,
|
||||
closeModal: () => undefined,
|
||||
};
|
||||
|
||||
const UploadModalContext = createContext<UploadModalContextValue>(defaultValue);
|
||||
|
||||
export function UploadModalProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const openModal = useCallback((nextFiles: File[]) => {
|
||||
setFiles(nextFiles);
|
||||
setIsVisible(true);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setIsVisible(false);
|
||||
setFiles([]);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<UploadModalContextValue>(
|
||||
() => ({ isVisible, files, openModal, closeModal }),
|
||||
[isVisible, files, openModal, closeModal],
|
||||
);
|
||||
|
||||
return <UploadModalContext.Provider value={value}>{children}</UploadModalContext.Provider>;
|
||||
}
|
||||
|
||||
export function useUploadModalContext() {
|
||||
return useContext(UploadModalContext);
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ export * from './SetConvoContext';
|
|||
export * from './SearchContext';
|
||||
export * from './BadgeRowContext';
|
||||
export * from './DragDropContext';
|
||||
export * from './UploadModalContext';
|
||||
export * from './ArtifactsContext';
|
||||
export * from './PromptGroupsContext';
|
||||
export * from './MessagesViewContext';
|
||||
|
|
|
|||
|
|
@ -2,179 +2,114 @@ import React, { useMemo } from 'react';
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { OGDialog, OGDialogTemplate } from '@librechat/client';
|
||||
import {
|
||||
ImageUpIcon,
|
||||
FileSearch,
|
||||
ImageUpIcon,
|
||||
FileType2Icon,
|
||||
FileImageIcon,
|
||||
TerminalSquareIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Constants,
|
||||
Providers,
|
||||
inferMimeType,
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
isBedrockDocumentType,
|
||||
defaultAgentCapabilities,
|
||||
isDocumentSupportedProvider,
|
||||
} from 'librechat-data-provider';
|
||||
import {
|
||||
useAgentToolPermissions,
|
||||
useAgentCapabilities,
|
||||
useGetAgentsConfig,
|
||||
useLocalize,
|
||||
useUploadOptions,
|
||||
useFileUploadRouter,
|
||||
useAgentToolPermissions,
|
||||
} from '~/hooks';
|
||||
import { useDragDropContext, useUploadModalContext } from '~/Providers';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
import { useDragDropContext } from '~/Providers';
|
||||
|
||||
interface DragDropModalProps {
|
||||
onOptionSelect: (option: EToolResources | undefined) => void;
|
||||
files: File[];
|
||||
isVisible: boolean;
|
||||
setShowModal: (showModal: boolean) => void;
|
||||
}
|
||||
|
||||
interface FileOption {
|
||||
label: string;
|
||||
value?: EToolResources;
|
||||
icon: React.JSX.Element;
|
||||
condition?: boolean;
|
||||
}
|
||||
|
||||
const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragDropModalProps) => {
|
||||
const DragDropModal = () => {
|
||||
const localize = useLocalize();
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
/** TODO: Ephemeral Agent Capabilities
|
||||
* Allow defining agent capabilities on a per-endpoint basis
|
||||
* Use definition for agents endpoint for ephemeral agents
|
||||
* */
|
||||
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||
const { isVisible, files, closeModal } = useUploadModalContext();
|
||||
const { conversationId, agentId, endpoint, endpointType, useResponsesApi } = useDragDropContext();
|
||||
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId ?? ''));
|
||||
const { fileSearchAllowedByAgent, codeAllowedByAgent, provider } = useAgentToolPermissions(
|
||||
agentId,
|
||||
ephemeralAgent,
|
||||
const ephemeralAgent = useRecoilValue(
|
||||
ephemeralAgentByConvoId(conversationId ?? Constants.NEW_CONVO),
|
||||
);
|
||||
const { provider } = useAgentToolPermissions(agentId, ephemeralAgent);
|
||||
const { getOptions } = useUploadOptions();
|
||||
const routeFiles = useFileUploadRouter();
|
||||
|
||||
const options = useMemo(() => {
|
||||
const _options: FileOption[] = [];
|
||||
let currentProvider = provider || endpoint;
|
||||
|
||||
// This will be removed in a future PR to formally normalize Providers comparisons to be case insensitive
|
||||
if (currentProvider?.toLowerCase() === Providers.OPENROUTER) {
|
||||
const isProviderDocSupported = useMemo(() => {
|
||||
let currentProvider = (provider || endpoint) ?? '';
|
||||
if (currentProvider.toLowerCase() === Providers.OPENROUTER) {
|
||||
currentProvider = Providers.OPENROUTER;
|
||||
}
|
||||
|
||||
/** Helper to get inferred MIME type for a file */
|
||||
const getFileType = (file: File) => inferMimeType(file.name, file.type);
|
||||
|
||||
const isAzureWithResponsesApi =
|
||||
(currentProvider === EModelEndpoint.azureOpenAI ||
|
||||
endpointType === EModelEndpoint.azureOpenAI) &&
|
||||
useResponsesApi === true;
|
||||
|
||||
// Check if provider supports document upload
|
||||
if (
|
||||
return (
|
||||
isDocumentSupportedProvider(endpointType) ||
|
||||
isDocumentSupportedProvider(currentProvider) ||
|
||||
isAzureWithResponsesApi
|
||||
) {
|
||||
const supportsImageDocVideoAudio =
|
||||
currentProvider === EModelEndpoint.google || currentProvider === Providers.OPENROUTER;
|
||||
const isBedrock =
|
||||
currentProvider === Providers.BEDROCK || endpointType === EModelEndpoint.bedrock;
|
||||
);
|
||||
}, [provider, endpoint, endpointType, useResponsesApi]);
|
||||
|
||||
const isValidProviderFile = (file: File): boolean => {
|
||||
const type = getFileType(file);
|
||||
if (supportsImageDocVideoAudio) {
|
||||
return (
|
||||
type?.startsWith('image/') ||
|
||||
type?.startsWith('video/') ||
|
||||
type?.startsWith('audio/') ||
|
||||
type === 'application/pdf'
|
||||
);
|
||||
}
|
||||
if (isBedrock) {
|
||||
return type?.startsWith('image/') || isBedrockDocumentType(type);
|
||||
}
|
||||
return type?.startsWith('image/') || type === 'application/pdf';
|
||||
};
|
||||
const getOptionMeta = (value: EToolResources | undefined) => {
|
||||
switch (value) {
|
||||
case EToolResources.file_search:
|
||||
return {
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
icon: <FileSearch className="icon-md" />,
|
||||
};
|
||||
case EToolResources.execute_code:
|
||||
return {
|
||||
label: localize('com_ui_upload_code_environment'),
|
||||
icon: <TerminalSquareIcon className="icon-md" />,
|
||||
};
|
||||
case EToolResources.context:
|
||||
return {
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
icon: <FileType2Icon className="icon-md" />,
|
||||
};
|
||||
default:
|
||||
return isProviderDocSupported
|
||||
? {
|
||||
label: localize('com_ui_upload_provider'),
|
||||
icon: <FileImageIcon className="icon-md" />,
|
||||
}
|
||||
: {
|
||||
label: localize('com_ui_upload_image_input'),
|
||||
icon: <ImageUpIcon className="icon-md" />,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const validFileTypes = files.every(isValidProviderFile);
|
||||
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_provider'),
|
||||
value: undefined,
|
||||
icon: <FileImageIcon className="icon-md" />,
|
||||
condition: validFileTypes,
|
||||
});
|
||||
} else {
|
||||
// Only show image upload option if all files are images and provider doesn't support documents
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_image_input'),
|
||||
value: undefined,
|
||||
icon: <ImageUpIcon className="icon-md" />,
|
||||
condition: files.every((file) => getFileType(file)?.startsWith('image/')),
|
||||
});
|
||||
}
|
||||
if (capabilities.fileSearchEnabled && fileSearchAllowedByAgent) {
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
value: EToolResources.file_search,
|
||||
icon: <FileSearch className="icon-md" />,
|
||||
});
|
||||
}
|
||||
if (capabilities.codeEnabled && codeAllowedByAgent) {
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_code_environment'),
|
||||
value: EToolResources.execute_code,
|
||||
icon: <TerminalSquareIcon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
if (capabilities.contextEnabled) {
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
value: EToolResources.context,
|
||||
icon: <FileType2Icon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
|
||||
return _options;
|
||||
}, [
|
||||
files,
|
||||
localize,
|
||||
provider,
|
||||
endpoint,
|
||||
endpointType,
|
||||
capabilities,
|
||||
useResponsesApi,
|
||||
codeAllowedByAgent,
|
||||
fileSearchAllowedByAgent,
|
||||
]);
|
||||
const options = useMemo(() => getOptions(files), [getOptions, files]);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OGDialog open={isVisible} onOpenChange={setShowModal}>
|
||||
<OGDialog open={isVisible} onOpenChange={(open) => !open && closeModal()}>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_ui_upload_type')}
|
||||
className="w-11/12 sm:w-[440px] md:w-[400px] lg:w-[360px]"
|
||||
main={
|
||||
<div className="flex flex-col gap-2">
|
||||
{options.map(
|
||||
(option, index) =>
|
||||
option.condition !== false && (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onOptionSelect(option.value)}
|
||||
className="flex items-center gap-2 rounded-lg p-2 hover:bg-surface-active-alt"
|
||||
>
|
||||
{option.icon}
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
{options.map((value) => {
|
||||
const { label, icon } = getOptionMeta(value);
|
||||
return (
|
||||
<button
|
||||
key={value ?? 'provider'}
|
||||
onClick={() => {
|
||||
routeFiles(files, value);
|
||||
closeModal();
|
||||
}}
|
||||
className="flex items-center gap-2 rounded-lg p-2 hover:bg-surface-active-alt"
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useDragHelpers } from '~/hooks';
|
||||
import DragDropOverlay from '~/components/Chat/Input/Files/DragDropOverlay';
|
||||
import DragDropModal from '~/components/Chat/Input/Files/DragDropModal';
|
||||
import { DragDropProvider } from '~/Providers';
|
||||
import { DragDropProvider, UploadModalProvider } from '~/Providers';
|
||||
import { useDragHelpers } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface DragDropWrapperProps {
|
||||
|
|
@ -9,10 +9,8 @@ interface DragDropWrapperProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export default function DragDropWrapper({ children, className }: DragDropWrapperProps) {
|
||||
const { isOver, canDrop, drop, showModal, setShowModal, draggedFiles, handleOptionSelect } =
|
||||
useDragHelpers();
|
||||
|
||||
function DragDropArea({ children, className }: DragDropWrapperProps) {
|
||||
const { isOver, canDrop, drop } = useDragHelpers();
|
||||
const isActive = canDrop && isOver;
|
||||
|
||||
return (
|
||||
|
|
@ -20,14 +18,17 @@ export default function DragDropWrapper({ children, className }: DragDropWrapper
|
|||
{children}
|
||||
{/** Always render overlay to avoid mount/unmount overhead */}
|
||||
<DragDropOverlay isActive={isActive} />
|
||||
<DragDropProvider>
|
||||
<DragDropModal
|
||||
files={draggedFiles}
|
||||
isVisible={showModal}
|
||||
setShowModal={setShowModal}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<DragDropModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DragDropWrapper({ children, className }: DragDropWrapperProps) {
|
||||
return (
|
||||
<DragDropProvider>
|
||||
<UploadModalProvider>
|
||||
<DragDropArea className={className}>{children}</DragDropArea>
|
||||
</UploadModalProvider>
|
||||
</DragDropProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export { default as useFileHandling, useFileHandlingNoChatContext } from './useF
|
|||
export { default as useFileDeletion } from './useFileDeletion';
|
||||
export { default as useUpdateFiles } from './useUpdateFiles';
|
||||
export { default as useDragHelpers } from './useDragHelpers';
|
||||
export { default as useUploadOptions } from './useUploadOptions';
|
||||
export { default as useFileUploadRouter } from './useFileUploadRouter';
|
||||
export { default as useFileMap } from './useFileMap';
|
||||
export { default as useSharePointPicker } from './useSharePointPicker';
|
||||
export { default as useSharePointDownload } from './useSharePointDownload';
|
||||
|
|
|
|||
|
|
@ -1,83 +1,61 @@
|
|||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useRef, useMemo, useCallback } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import { NativeTypes } from 'react-dnd-html5-backend';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
Tools,
|
||||
QueryKeys,
|
||||
Constants,
|
||||
inferMimeType,
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
AgentCapabilities,
|
||||
resolveEndpointType,
|
||||
isAssistantsEndpoint,
|
||||
getEndpointFileConfig,
|
||||
defaultAgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import type { DropTargetMonitor } from 'react-dnd';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import store, { ephemeralAgentByConvoId } from '~/store';
|
||||
import useFileHandling from './useFileHandling';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
import useFileUploadRouter from './useFileUploadRouter';
|
||||
import { useUploadModalContext } from '~/Providers';
|
||||
import useUploadOptions from './useUploadOptions';
|
||||
import useLocalize from '../useLocalize';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useDragHelpers() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const setEphemeralAgent = useSetRecoilState(
|
||||
ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO),
|
||||
);
|
||||
|
||||
const isAssistants = useMemo(
|
||||
() => isAssistantsEndpoint(conversation?.endpoint),
|
||||
[conversation?.endpoint],
|
||||
);
|
||||
|
||||
const { handleFiles } = useFileHandling();
|
||||
|
||||
const handleOptionSelect = useCallback(
|
||||
(toolResource: EToolResources | undefined) => {
|
||||
/** File search is not automatically enabled to simulate legacy behavior */
|
||||
if (toolResource && toolResource !== EToolResources.file_search) {
|
||||
setEphemeralAgent((prev) => ({
|
||||
...prev,
|
||||
[toolResource]: true,
|
||||
}));
|
||||
}
|
||||
handleFiles(draggedFiles, toolResource);
|
||||
setShowModal(false);
|
||||
setDraggedFiles([]);
|
||||
},
|
||||
[draggedFiles, handleFiles, setEphemeralAgent],
|
||||
);
|
||||
const { getOptions } = useUploadOptions();
|
||||
const routeFiles = useFileUploadRouter();
|
||||
const { openModal } = useUploadModalContext();
|
||||
|
||||
/** Use refs to avoid re-creating the drop handler */
|
||||
const handleFilesRef = useRef(handleFiles);
|
||||
const conversationRef = useRef(conversation);
|
||||
const getOptionsRef = useRef(getOptions);
|
||||
const routeFilesRef = useRef(routeFiles);
|
||||
const openModalRef = useRef(openModal);
|
||||
const isAssistantsRef = useRef(isAssistants);
|
||||
|
||||
handleFilesRef.current = handleFiles;
|
||||
conversationRef.current = conversation;
|
||||
getOptionsRef.current = getOptions;
|
||||
routeFilesRef.current = routeFiles;
|
||||
openModalRef.current = openModal;
|
||||
isAssistantsRef.current = isAssistants;
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(item: { files: File[] }) => {
|
||||
/** Early block: leverage endpoint file config to prevent drag/drop on disabled endpoints */
|
||||
const currentEndpoint = conversationRef.current?.endpoint ?? 'default';
|
||||
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
/** Get agent data from cache; if absent, provider-specific file config restrictions are bypassed client-side */
|
||||
const agentId = conversationRef.current?.agent_id;
|
||||
const agent = agentId
|
||||
? queryClient.getQueryData<t.Agent>([QueryKeys.agent, agentId])
|
||||
: undefined;
|
||||
|
||||
const currentEndpointType = resolveEndpointType(
|
||||
endpointsConfig,
|
||||
currentEndpoint,
|
||||
|
|
@ -85,66 +63,35 @@ export default function useDragHelpers() {
|
|||
);
|
||||
const cfg = queryClient.getQueryData<t.TFileConfig>([QueryKeys.fileConfig]);
|
||||
if (cfg) {
|
||||
const mergedCfg = mergeFileConfig(cfg);
|
||||
const endpointCfg = getEndpointFileConfig({
|
||||
fileConfig: mergedCfg,
|
||||
fileConfig: mergeFileConfig(cfg),
|
||||
endpoint: currentEndpoint,
|
||||
endpointType: currentEndpointType,
|
||||
});
|
||||
if (endpointCfg?.disabled === true) {
|
||||
showToast({
|
||||
message: localize('com_ui_attach_error_disabled'),
|
||||
status: 'error',
|
||||
});
|
||||
showToast({ message: localize('com_ui_attach_error_disabled'), status: 'error' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAssistants) {
|
||||
handleFilesRef.current(item.files);
|
||||
/** Assistants do not use the upload-option flow */
|
||||
if (isAssistantsRef.current) {
|
||||
routeFilesRef.current(item.files);
|
||||
return;
|
||||
}
|
||||
|
||||
const agentsConfig = endpointsConfig?.[EModelEndpoint.agents];
|
||||
const capabilities = agentsConfig?.capabilities ?? defaultAgentCapabilities;
|
||||
const fileSearchEnabled = capabilities.includes(AgentCapabilities.file_search) === true;
|
||||
const codeEnabled = capabilities.includes(AgentCapabilities.execute_code) === true;
|
||||
const contextEnabled = capabilities.includes(AgentCapabilities.context) === true;
|
||||
|
||||
let fileSearchAllowedByAgent = true;
|
||||
let codeAllowedByAgent = true;
|
||||
|
||||
if (agentId && !isEphemeralAgent(agentId)) {
|
||||
if (agent) {
|
||||
const agentTools = agent.tools as string[] | undefined;
|
||||
fileSearchAllowedByAgent = agentTools?.includes(Tools.file_search) ?? false;
|
||||
codeAllowedByAgent = agentTools?.includes(Tools.execute_code) ?? false;
|
||||
} else {
|
||||
fileSearchAllowedByAgent = false;
|
||||
codeAllowedByAgent = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine if dragged files are all images (enables the base image option) */
|
||||
const allImages = item.files.every((f) =>
|
||||
inferMimeType(f.name, f.type)?.startsWith('image/'),
|
||||
);
|
||||
|
||||
const shouldShowModal =
|
||||
allImages ||
|
||||
(fileSearchEnabled && fileSearchAllowedByAgent) ||
|
||||
(codeEnabled && codeAllowedByAgent) ||
|
||||
contextEnabled;
|
||||
|
||||
if (!shouldShowModal) {
|
||||
// Fallback: directly handle files without showing modal
|
||||
handleFilesRef.current(item.files);
|
||||
const options = getOptionsRef.current(item.files);
|
||||
if (options.length === 0) {
|
||||
showToast({ message: localize('com_error_files_unsupported'), status: 'error' });
|
||||
return;
|
||||
}
|
||||
setDraggedFiles(item.files);
|
||||
setShowModal(true);
|
||||
if (options.length === 1) {
|
||||
routeFilesRef.current(item.files, options[0]);
|
||||
return;
|
||||
}
|
||||
openModalRef.current(item.files);
|
||||
},
|
||||
[isAssistants, queryClient, showToast, localize],
|
||||
[queryClient, showToast, localize],
|
||||
);
|
||||
|
||||
const [{ canDrop, isOver }, drop] = useDrop(
|
||||
|
|
@ -152,23 +99,13 @@ export default function useDragHelpers() {
|
|||
accept: [NativeTypes.FILE],
|
||||
drop: handleDrop,
|
||||
canDrop: () => true,
|
||||
collect: (monitor: DropTargetMonitor) => {
|
||||
/** Optimize collect to reduce re-renders */
|
||||
const isOver = monitor.isOver();
|
||||
const canDrop = monitor.canDrop();
|
||||
return { isOver, canDrop };
|
||||
},
|
||||
collect: (monitor: DropTargetMonitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
}),
|
||||
[handleDrop],
|
||||
);
|
||||
|
||||
return {
|
||||
canDrop,
|
||||
isOver,
|
||||
drop,
|
||||
showModal,
|
||||
setShowModal,
|
||||
draggedFiles,
|
||||
handleOptionSelect,
|
||||
};
|
||||
return { canDrop, isOver, drop };
|
||||
}
|
||||
|
|
|
|||
31
client/src/hooks/Files/useFileUploadRouter.ts
Normal file
31
client/src/hooks/Files/useFileUploadRouter.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Constants, EToolResources } from 'librechat-data-provider';
|
||||
import store, { ephemeralAgentByConvoId } from '~/store';
|
||||
import useFileHandling from './useFileHandling';
|
||||
|
||||
/**
|
||||
* Returns a function that attaches files to a chosen upload destination, enabling the
|
||||
* matching ephemeral-agent capability first (file search is left for explicit opt-in to
|
||||
* preserve legacy behavior). Shared by the paste, drag, and modal flows.
|
||||
*/
|
||||
export default function useFileUploadRouter() {
|
||||
const { handleFiles } = useFileHandling();
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const setEphemeralAgent = useSetRecoilState(
|
||||
ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO),
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(files: File[], toolResource?: EToolResources) => {
|
||||
if (toolResource && toolResource !== EToolResources.file_search) {
|
||||
setEphemeralAgent((prev) => ({
|
||||
...prev,
|
||||
[toolResource]: true,
|
||||
}));
|
||||
}
|
||||
handleFiles(files, toolResource);
|
||||
},
|
||||
[handleFiles, setEphemeralAgent],
|
||||
);
|
||||
}
|
||||
80
client/src/hooks/Files/useUploadOptions.ts
Normal file
80
client/src/hooks/Files/useUploadOptions.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
Tools,
|
||||
Constants,
|
||||
mergeFileConfig,
|
||||
getEndpointFileConfig,
|
||||
defaultAgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import type { EToolResources } from 'librechat-data-provider';
|
||||
import useAgentToolPermissions from '~/hooks/Agents/useAgentToolPermissions';
|
||||
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
|
||||
import useGetAgentsConfig from '~/hooks/Agents/useGetAgentsConfig';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
import { getViableUploadOptions } from '~/utils';
|
||||
import { useDragDropContext } from '~/Providers';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
|
||||
/**
|
||||
* Resolves which upload destinations a file set can be routed to, plus whether uploads are
|
||||
* disabled for the endpoint. Shared by the paste, drag, and modal flows so they decide
|
||||
* consistently from one source.
|
||||
*/
|
||||
export default function useUploadOptions() {
|
||||
const { conversationId, agentId, endpoint, endpointType, useResponsesApi } = useDragDropContext();
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||
const ephemeralAgent = useRecoilValue(
|
||||
ephemeralAgentByConvoId(conversationId ?? Constants.NEW_CONVO),
|
||||
);
|
||||
const { provider, tools } = useAgentToolPermissions(agentId, ephemeralAgent);
|
||||
const { data: fileConfig = null } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tools are offerable unless a saved agent omits them; in direct/ephemeral chats selecting
|
||||
* one enables the ephemeral capability, matching the original drag-and-drop behavior.
|
||||
*/
|
||||
const isSavedAgent = agentId != null && agentId !== '' && !isEphemeralAgent(agentId);
|
||||
const fileSearchAllowedByAgent = !isSavedAgent || (tools?.includes(Tools.file_search) ?? false);
|
||||
const codeAllowedByAgent = !isSavedAgent || (tools?.includes(Tools.execute_code) ?? false);
|
||||
|
||||
const endpointFileConfig = getEndpointFileConfig({ fileConfig, endpoint, endpointType });
|
||||
const uploadsDisabled = endpointFileConfig.disabled === true;
|
||||
const endpointSupportedMimeTypes = endpointFileConfig.supportedMimeTypes;
|
||||
|
||||
const getOptions = useCallback(
|
||||
(files: File[]): (EToolResources | undefined)[] =>
|
||||
getViableUploadOptions(files, {
|
||||
provider,
|
||||
endpoint,
|
||||
endpointType,
|
||||
useResponsesApi,
|
||||
fileSearchEnabled: capabilities.fileSearchEnabled,
|
||||
codeEnabled: capabilities.codeEnabled,
|
||||
contextEnabled: capabilities.contextEnabled,
|
||||
fileSearchAllowedByAgent,
|
||||
codeAllowedByAgent,
|
||||
fileConfig,
|
||||
endpointSupportedMimeTypes,
|
||||
}),
|
||||
[
|
||||
provider,
|
||||
endpoint,
|
||||
endpointType,
|
||||
useResponsesApi,
|
||||
capabilities.fileSearchEnabled,
|
||||
capabilities.codeEnabled,
|
||||
capabilities.contextEnabled,
|
||||
fileSearchAllowedByAgent,
|
||||
codeAllowedByAgent,
|
||||
fileConfig,
|
||||
endpointSupportedMimeTypes,
|
||||
],
|
||||
);
|
||||
|
||||
return { getOptions, uploadsDisabled };
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { EToolResources, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TEndpointOption } from 'librechat-data-provider';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import {
|
||||
|
|
@ -18,11 +20,13 @@ import {
|
|||
} from '~/utils';
|
||||
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import useFileUploadRouter from '~/hooks/Files/useFileUploadRouter';
|
||||
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import useFileHandling from '~/hooks/Files/useFileHandling';
|
||||
import useUploadOptions from '~/hooks/Files/useUploadOptions';
|
||||
import { useInteractionHealthCheck } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers/ChatContext';
|
||||
import { useUploadModalContext } from '~/Providers';
|
||||
import { globalAudioId } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
|
@ -46,7 +50,10 @@ export default function useTextarea({
|
|||
const getSender = useGetSender();
|
||||
const isComposing = useRef(false);
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const { handleFiles } = useFileHandling();
|
||||
const { showToast } = useToastContext();
|
||||
const { getOptions: getUploadOptions, uploadsDisabled } = useUploadOptions();
|
||||
const routeFiles = useFileUploadRouter();
|
||||
const { openModal } = useUploadModalContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const checkHealth = useInteractionHealthCheck();
|
||||
const enterToSend = useRecoilValue(store.enterToSend);
|
||||
|
|
@ -285,10 +292,47 @@ export default function useTextarea({
|
|||
});
|
||||
timestampedFiles.push(newFile);
|
||||
}
|
||||
handleFiles(timestampedFiles);
|
||||
|
||||
if (uploadsDisabled) {
|
||||
showToast({ message: localize('com_ui_attach_error_disabled'), status: 'error' });
|
||||
setFilesLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
/** Assistants use their own upload path; bypass option resolution like drag-and-drop does */
|
||||
if (isAssistantsEndpoint(conversation?.endpoint)) {
|
||||
routeFiles(timestampedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = getUploadOptions(timestampedFiles);
|
||||
if (options.length === 0) {
|
||||
showToast({ message: localize('com_error_files_unsupported'), status: 'error' });
|
||||
setFilesLoading(false);
|
||||
return;
|
||||
}
|
||||
if (options.length === 1) {
|
||||
routeFiles(timestampedFiles, options[0]);
|
||||
if (options[0] === EToolResources.context) {
|
||||
showToast({ message: localize('com_ui_file_attached_as_text'), status: 'info' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setFilesLoading(false);
|
||||
openModal(timestampedFiles);
|
||||
}
|
||||
},
|
||||
[handleFiles, setFilesLoading, textAreaRef],
|
||||
[
|
||||
localize,
|
||||
showToast,
|
||||
openModal,
|
||||
routeFiles,
|
||||
conversation,
|
||||
textAreaRef,
|
||||
uploadsDisabled,
|
||||
setFilesLoading,
|
||||
getUploadOptions,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -373,6 +373,7 @@
|
|||
"com_error_files_dupe": "Duplicate file detected.",
|
||||
"com_error_files_empty": "Empty files are not allowed.",
|
||||
"com_error_files_process": "An error occurred while processing the file.",
|
||||
"com_error_files_unsupported": "This file type can't be attached here.",
|
||||
"com_error_files_upload": "An error occurred while uploading the file.",
|
||||
"com_error_files_upload_canceled": "The file upload request was canceled. Note: the file upload may still be processing and will need to be manually deleted.",
|
||||
"com_error_files_upload_too_large": "The file is too large. Please upload a file smaller than {{0}} MB",
|
||||
|
|
@ -1147,6 +1148,7 @@
|
|||
"com_ui_field_max_length": "{{field}} must be less than {{length}} characters",
|
||||
"com_ui_field_required": "This field is required",
|
||||
"com_ui_file": "File",
|
||||
"com_ui_file_attached_as_text": "Attached as text",
|
||||
"com_ui_file_input_avatar_label": "File input for avatar",
|
||||
"com_ui_file_modified": "Modified",
|
||||
"com_ui_file_size": "File Size",
|
||||
|
|
|
|||
147
client/src/utils/__tests__/getViableUploadOptions.spec.ts
Normal file
147
client/src/utils/__tests__/getViableUploadOptions.spec.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { EToolResources } from 'librechat-data-provider';
|
||||
import type { FileConfig } from 'librechat-data-provider';
|
||||
import { getViableUploadOptions, type UploadOptionContext } from '../files';
|
||||
|
||||
const XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
|
||||
/** context accepts plain text + csv (text), pdf + xlsx (ocr); nothing else */
|
||||
const fileConfig = {
|
||||
text: { supportedMimeTypes: [/^text\/(plain|csv)$/] },
|
||||
ocr: {
|
||||
supportedMimeTypes: [
|
||||
/^application\/pdf$/,
|
||||
/^application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet$/,
|
||||
],
|
||||
},
|
||||
stt: { supportedMimeTypes: [] },
|
||||
} as unknown as FileConfig;
|
||||
|
||||
const baseCtx = (over: Partial<UploadOptionContext> = {}): UploadOptionContext => ({
|
||||
provider: 'anthropic',
|
||||
endpoint: 'anthropic',
|
||||
endpointType: 'anthropic',
|
||||
useResponsesApi: false,
|
||||
fileSearchEnabled: true,
|
||||
codeEnabled: true,
|
||||
contextEnabled: true,
|
||||
fileSearchAllowedByAgent: true,
|
||||
codeAllowedByAgent: true,
|
||||
fileConfig,
|
||||
...over,
|
||||
});
|
||||
|
||||
const file = (type: string, name: string) => new File(['x'], name, { type });
|
||||
|
||||
describe('getViableUploadOptions', () => {
|
||||
it('returns empty for no files', () => {
|
||||
expect(getViableUploadOptions([], baseCtx())).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty when a file type cannot be inferred', () => {
|
||||
expect(getViableUploadOptions([file('', 'mystery.unknownext')], baseCtx())).toEqual([]);
|
||||
});
|
||||
|
||||
describe('Anthropic (PDF/image only for provider attach)', () => {
|
||||
it('routes a spreadsheet to code + text, not the provider', () => {
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], baseCtx())).toEqual([
|
||||
EToolResources.execute_code,
|
||||
EToolResources.context,
|
||||
]);
|
||||
});
|
||||
|
||||
it('offers every destination for a PDF', () => {
|
||||
expect(getViableUploadOptions([file('application/pdf', 'doc.pdf')], baseCtx())).toEqual([
|
||||
undefined,
|
||||
EToolResources.file_search,
|
||||
EToolResources.execute_code,
|
||||
EToolResources.context,
|
||||
]);
|
||||
});
|
||||
|
||||
it('yields a single option for a zip (code only) so it can auto-route', () => {
|
||||
expect(getViableUploadOptions([file('application/zip', 'a.zip')], baseCtx())).toEqual([
|
||||
EToolResources.execute_code,
|
||||
]);
|
||||
});
|
||||
|
||||
it('attaches a PDF directly to the provider when capabilities are off', () => {
|
||||
const ctx = baseCtx({ fileSearchEnabled: false, codeEnabled: false, contextEnabled: false });
|
||||
expect(getViableUploadOptions([file('application/pdf', 'doc.pdf')], ctx)).toEqual([
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns nothing for a spreadsheet when no capabilities are enabled', () => {
|
||||
const ctx = baseCtx({ fileSearchEnabled: false, codeEnabled: false, contextEnabled: false });
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], ctx)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider-specific direct attachment', () => {
|
||||
it('lets Google attach video directly', () => {
|
||||
const ctx = baseCtx({
|
||||
provider: 'google',
|
||||
endpoint: 'google',
|
||||
endpointType: 'google',
|
||||
fileSearchEnabled: false,
|
||||
codeEnabled: false,
|
||||
contextEnabled: false,
|
||||
});
|
||||
expect(getViableUploadOptions([file('video/mp4', 'clip.mp4')], ctx)).toEqual([undefined]);
|
||||
});
|
||||
|
||||
it('does not let Anthropic attach video directly', () => {
|
||||
const ctx = baseCtx({
|
||||
fileSearchEnabled: false,
|
||||
codeEnabled: false,
|
||||
contextEnabled: false,
|
||||
});
|
||||
expect(getViableUploadOptions([file('video/mp4', 'clip.mp4')], ctx)).toEqual([]);
|
||||
});
|
||||
|
||||
it('lets Bedrock attach a spreadsheet directly via its document allowlist', () => {
|
||||
const ctx = baseCtx({
|
||||
provider: 'bedrock',
|
||||
endpoint: 'bedrock',
|
||||
endpointType: 'bedrock',
|
||||
fileSearchEnabled: false,
|
||||
codeEnabled: false,
|
||||
contextEnabled: false,
|
||||
});
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], ctx)).toEqual([undefined]);
|
||||
});
|
||||
|
||||
it('honors a permissive custom endpoint config for direct attach', () => {
|
||||
const ctx = baseCtx({
|
||||
provider: 'MyGateway',
|
||||
endpoint: 'MyGateway',
|
||||
endpointType: 'custom',
|
||||
fileSearchEnabled: false,
|
||||
codeEnabled: false,
|
||||
contextEnabled: false,
|
||||
endpointSupportedMimeTypes: [/.*/],
|
||||
});
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], ctx)).toEqual([undefined]);
|
||||
});
|
||||
|
||||
it('does not treat a non-permissive custom config as broad provider support', () => {
|
||||
const ctx = baseCtx({
|
||||
provider: 'MyGateway',
|
||||
endpoint: 'MyGateway',
|
||||
endpointType: 'custom',
|
||||
fileSearchEnabled: false,
|
||||
codeEnabled: false,
|
||||
contextEnabled: false,
|
||||
endpointSupportedMimeTypes: [/^application\/pdf$/],
|
||||
});
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], ctx)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('drops an option when the agent disallows it', () => {
|
||||
const ctx = baseCtx({ contextEnabled: false, fileSearchEnabled: false });
|
||||
expect(getViableUploadOptions([file(XLSX, 'report.xlsx')], ctx)).toEqual([
|
||||
EToolResources.execute_code,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -8,10 +8,17 @@ import {
|
|||
} from '@librechat/client';
|
||||
import {
|
||||
megabyte,
|
||||
Providers,
|
||||
QueryKeys,
|
||||
inferMimeType,
|
||||
excelMimeTypes,
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
retrievalMimeTypes,
|
||||
isBedrockDocumentType,
|
||||
isPermissiveMimeConfig,
|
||||
codeInterpreterMimeTypes,
|
||||
isDocumentSupportedProvider,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider';
|
||||
|
|
@ -318,6 +325,111 @@ export const validateFiles = ({
|
|||
return true;
|
||||
};
|
||||
|
||||
export type UploadOptionContext = {
|
||||
provider?: string | null;
|
||||
endpoint?: string | null;
|
||||
endpointType?: string | null;
|
||||
useResponsesApi?: boolean;
|
||||
fileSearchEnabled: boolean;
|
||||
codeEnabled: boolean;
|
||||
contextEnabled: boolean;
|
||||
fileSearchAllowedByAgent: boolean;
|
||||
codeAllowedByAgent: boolean;
|
||||
fileConfig: FileConfig | null;
|
||||
endpointSupportedMimeTypes?: RegExp[];
|
||||
};
|
||||
|
||||
const isProviderAttachType = (type: string, ctx: UploadOptionContext): boolean => {
|
||||
let currentProvider = (ctx.provider || ctx.endpoint) ?? '';
|
||||
if (currentProvider.toLowerCase() === Providers.OPENROUTER) {
|
||||
currentProvider = Providers.OPENROUTER;
|
||||
}
|
||||
const isAzureWithResponsesApi =
|
||||
(currentProvider === EModelEndpoint.azureOpenAI ||
|
||||
ctx.endpointType === EModelEndpoint.azureOpenAI) &&
|
||||
ctx.useResponsesApi === true;
|
||||
|
||||
if (
|
||||
isDocumentSupportedProvider(ctx.endpointType) ||
|
||||
isDocumentSupportedProvider(currentProvider) ||
|
||||
isAzureWithResponsesApi
|
||||
) {
|
||||
/** Custom endpoints that the admin opened up (permissive config) honor that allowlist,
|
||||
* matching the file picker; an inherited default config is not treated as opened up. */
|
||||
if (
|
||||
ctx.endpointType === EModelEndpoint.custom &&
|
||||
ctx.endpointSupportedMimeTypes != null &&
|
||||
isPermissiveMimeConfig(ctx.endpointSupportedMimeTypes)
|
||||
) {
|
||||
return checkType(type, ctx.endpointSupportedMimeTypes);
|
||||
}
|
||||
if (currentProvider === EModelEndpoint.google || currentProvider === Providers.OPENROUTER) {
|
||||
return (
|
||||
type.startsWith('image/') ||
|
||||
type.startsWith('video/') ||
|
||||
type.startsWith('audio/') ||
|
||||
type === 'application/pdf'
|
||||
);
|
||||
}
|
||||
if (currentProvider === Providers.BEDROCK || ctx.endpointType === EModelEndpoint.bedrock) {
|
||||
return type.startsWith('image/') || isBedrockDocumentType(type);
|
||||
}
|
||||
return type.startsWith('image/') || type === 'application/pdf';
|
||||
}
|
||||
return type.startsWith('image/');
|
||||
};
|
||||
|
||||
const isContextType = (type: string, fileConfig: FileConfig | null): boolean =>
|
||||
checkType(type, [
|
||||
...(fileConfig?.text?.supportedMimeTypes || []),
|
||||
...(fileConfig?.ocr?.supportedMimeTypes || []),
|
||||
...(fileConfig?.stt?.supportedMimeTypes || []),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Upload destinations a file set can be routed to, given the active endpoint and agent
|
||||
* capabilities. `undefined` is direct provider attachment; the rest are tool resources.
|
||||
* Each option requires every file to be valid for it, so the caller can decide between
|
||||
* auto-routing (one option), prompting (multiple), or rejecting (none).
|
||||
*/
|
||||
export const getViableUploadOptions = (
|
||||
fileList: File[],
|
||||
ctx: UploadOptionContext,
|
||||
): (EToolResources | undefined)[] => {
|
||||
if (fileList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const types = fileList.map((file) => inferMimeType(file.name, file.type));
|
||||
if (types.some((type) => !type)) {
|
||||
return [];
|
||||
}
|
||||
const every = (predicate: (type: string) => boolean) =>
|
||||
types.every((type) => predicate(type as string));
|
||||
|
||||
const options: (EToolResources | undefined)[] = [];
|
||||
if (every((type) => isProviderAttachType(type, ctx))) {
|
||||
options.push(undefined);
|
||||
}
|
||||
if (
|
||||
ctx.fileSearchEnabled &&
|
||||
ctx.fileSearchAllowedByAgent &&
|
||||
every((type) => !type.startsWith('image/') && checkType(type, retrievalMimeTypes))
|
||||
) {
|
||||
options.push(EToolResources.file_search);
|
||||
}
|
||||
if (
|
||||
ctx.codeEnabled &&
|
||||
ctx.codeAllowedByAgent &&
|
||||
every((type) => checkType(type, codeInterpreterMimeTypes))
|
||||
) {
|
||||
options.push(EToolResources.execute_code);
|
||||
}
|
||||
if (ctx.contextEnabled && every((type) => isContextType(type, ctx.fileConfig))) {
|
||||
options.push(EToolResources.context);
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
export function sortPagesByRelevance(
|
||||
pages: number[],
|
||||
pageRelevance: Record<number, number>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue