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",