diff --git a/client/src/components/Chat/Input/Files/DragDropOverlay.tsx b/client/src/components/Chat/Input/Files/DragDropOverlay.tsx index 9966a8d02..f5f45e2b8 100644 --- a/client/src/components/Chat/Input/Files/DragDropOverlay.tsx +++ b/client/src/components/Chat/Input/Files/DragDropOverlay.tsx @@ -1,62 +1,102 @@ -export default function DragDropOverlay() { - return ( -
- - - - - - - - - - - - - - - - - -

Add anything

-

Drop any file here to add it to the conversation

-
- ); +import { memo } from 'react'; +import { useLocalize } from '~/hooks'; + +interface DragDropOverlayProps { + isActive: boolean; } + +const DragDropOverlay = memo(({ isActive }: DragDropOverlayProps) => { + const localize = useLocalize(); + return ( + <> + {/** Modal backdrop overlay */} +
+ {/** Main content overlay */} +
+ {/** Content area with subtle background */} +
+ + + + + + + + + + + + + + + + + +

{localize('com_ui_upload_files')}

+

{localize('com_ui_drag_drop')}

+
+
+ + ); +}); + +DragDropOverlay.displayName = 'DragDropOverlay'; + +export default DragDropOverlay; diff --git a/client/src/components/Chat/Input/Files/DragDropWrapper.tsx b/client/src/components/Chat/Input/Files/DragDropWrapper.tsx index db18b75b1..1a3698e09 100644 --- a/client/src/components/Chat/Input/Files/DragDropWrapper.tsx +++ b/client/src/components/Chat/Input/Files/DragDropWrapper.tsx @@ -17,7 +17,8 @@ export default function DragDropWrapper({ children, className }: DragDropWrapper return (
{children} - {isActive && } + {/** Always render overlay to avoid mount/unmount overhead */} + ([]); const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined; - const agentId = conversation?.agent_id ?? ''; - const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(agentId); const setEphemeralAgent = useSetRecoilState( ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO), ); - const handleOptionSelect = (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([]); - }; - const isAssistants = useMemo( () => isAssistantsEndpoint(conversation?.endpoint), [conversation?.endpoint], @@ -51,47 +36,95 @@ export default function useDragHelpers() { overrideEndpoint: isAssistants ? undefined : EModelEndpoint.agents, }); + 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], + ); + + /** Use refs to avoid re-creating the drop handler */ + const handleFilesRef = useRef(handleFiles); + const conversationRef = useRef(conversation); + + handleFilesRef.current = handleFiles; + conversationRef.current = conversation; + + const handleDrop = useCallback( + (item: { files: File[] }) => { + if (isAssistants) { + handleFilesRef.current(item.files); + return; + } + + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + 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 ocrEnabled = capabilities.includes(AgentCapabilities.ocr) === true; + + /** Get agent permissions at drop time */ + const agentId = conversationRef.current?.agent_id; + let fileSearchAllowedByAgent = true; + let codeAllowedByAgent = true; + + if (agentId && agentId !== Constants.EPHEMERAL_AGENT_ID) { + /** Agent data from cache */ + const agent = queryClient.getQueryData([QueryKeys.agent, 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 { + /** If agent exists but not found, disallow */ + fileSearchAllowedByAgent = false; + codeAllowedByAgent = false; + } + } + + /** Determine if dragged files are all images (enables the base image option) */ + const allImages = item.files.every((f) => f.type?.startsWith('image/')); + + const shouldShowModal = + allImages || + (fileSearchEnabled && fileSearchAllowedByAgent) || + (codeEnabled && codeAllowedByAgent) || + ocrEnabled; + + if (!shouldShowModal) { + // Fallback: directly handle files without showing modal + handleFilesRef.current(item.files); + return; + } + setDraggedFiles(item.files); + setShowModal(true); + }, + [isAssistants, queryClient], + ); + const [{ canDrop, isOver }, drop] = useDrop( () => ({ accept: [NativeTypes.FILE], - drop(item: { files: File[] }) { - console.log('drop', item.files); - if (isAssistants) { - handleFiles(item.files); - return; - } - - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); - 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 ocrEnabled = capabilities.includes(AgentCapabilities.ocr) === true; - - /** Determine if dragged files are all images (enables the base image option) */ - const allImages = item.files.every((f) => f.type?.startsWith('image/')); - - const shouldShowModal = - allImages || - (fileSearchEnabled && fileSearchAllowedByAgent) || - (codeEnabled && codeAllowedByAgent) || - ocrEnabled; - - if (!shouldShowModal) { - // Fallback: directly handle files without showing modal - handleFiles(item.files); - return; - } - setDraggedFiles(item.files); - setShowModal(true); - }, + drop: handleDrop, canDrop: () => true, - collect: (monitor: DropTargetMonitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), + collect: (monitor: DropTargetMonitor) => { + /** Optimize collect to reduce re-renders */ + const isOver = monitor.isOver(); + const canDrop = monitor.canDrop(); + return { isOver, canDrop }; + }, }), - [handleFiles], + [handleDrop], ); return { diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 2e67a4b0b..75219abb8 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -848,7 +848,7 @@ "com_ui_download_backup": "Download Backup Codes", "com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device", "com_ui_download_error": "Error downloading file. The file may have been deleted.", - "com_ui_drag_drop": "something needs to go here. was empty", + "com_ui_drag_drop": "Drop any file here to add it to the conversation", "com_ui_dropdown_variables": "Dropdown variables:", "com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`", "com_ui_duplicate": "Duplicate",