mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 18:00:15 +01:00
📬 refactor: Improved Rendering and Localization for Drag & Drop Files (#9547)
* 📬 refactor: Improved Rendering and Localization for Drag & Drop Files
- Refactored DragDropOverlay to use memoization and props for active state management.
- Updated the overlay to always render, reducing mount/unmount overhead.
- Improved user experience with localized text for drag-and-drop instructions.
- Enhanced file handling logic in useDragHelpers for better performance and clarity.
* fix: agent data retrieval in drag helper
This commit is contained in:
parent
1247207afe
commit
749f539dfc
4 changed files with 190 additions and 116 deletions
|
|
@ -1,18 +1,52 @@
|
||||||
export default function DragDropOverlay() {
|
import { memo } from 'react';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface DragDropOverlayProps {
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragDropOverlay = memo(({ isActive }: DragDropOverlayProps) => {
|
||||||
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{/** Modal backdrop overlay */}
|
||||||
<div
|
<div
|
||||||
className="bg-surface-primary/85 fixed inset-0 z-[9999] flex flex-col items-center justify-center
|
className={`fixed inset-0 z-[9998] transition-opacity duration-200 ease-in-out ${
|
||||||
gap-2 text-text-primary
|
isActive
|
||||||
backdrop-blur-[4px] transition-all duration-200
|
? 'pointer-events-auto visible opacity-100'
|
||||||
ease-in-out animate-in fade-in
|
: 'pointer-events-none invisible opacity-0'
|
||||||
zoom-in-95 hover:backdrop-blur-sm"
|
} `}
|
||||||
|
style={{
|
||||||
|
/** Semi-transparent black overlay that works in both themes */
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
willChange: 'opacity',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/** Main content overlay */}
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-[9999] flex flex-col items-center justify-center gap-2 text-text-primary transition-all duration-200 ease-in-out ${
|
||||||
|
isActive
|
||||||
|
? 'pointer-events-auto visible opacity-100'
|
||||||
|
: 'pointer-events-none invisible opacity-0'
|
||||||
|
} `}
|
||||||
|
style={{
|
||||||
|
transform: isActive ? 'scale(1)' : 'scale(0.95)',
|
||||||
|
/** Use will-change to hint browser about upcoming changes */
|
||||||
|
willChange: 'opacity, transform',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{/** Content area with subtle background */}
|
||||||
|
<div className="bg-surface-primary/95 flex flex-col items-center rounded-lg p-8 shadow-xl">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 132 108"
|
viewBox="0 0 132 108"
|
||||||
fill="none"
|
fill="none"
|
||||||
width="132"
|
width="132"
|
||||||
height="108"
|
height="108"
|
||||||
|
style={{
|
||||||
|
transform: isActive ? 'translateY(0)' : 'translateY(-10px)',
|
||||||
|
transition: 'transform 0.2s ease-in-out',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0_3605_64419)">
|
<g clipPath="url(#clip0_3605_64419)">
|
||||||
<path
|
<path
|
||||||
|
|
@ -55,8 +89,14 @@ export default function DragDropOverlay() {
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<h3>Add anything</h3>
|
<h3 className="mt-4 text-lg font-semibold">{localize('com_ui_upload_files')}</h3>
|
||||||
<h4>Drop any file here to add it to the conversation</h4>
|
<h4 className="text-sm text-text-secondary">{localize('com_ui_drag_drop')}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DragDropOverlay.displayName = 'DragDropOverlay';
|
||||||
|
|
||||||
|
export default DragDropOverlay;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ export default function DragDropWrapper({ children, className }: DragDropWrapper
|
||||||
return (
|
return (
|
||||||
<div ref={drop} className={cn('relative flex h-full w-full', className)}>
|
<div ref={drop} className={cn('relative flex h-full w-full', className)}>
|
||||||
{children}
|
{children}
|
||||||
{isActive && <DragDropOverlay />}
|
{/** Always render overlay to avoid mount/unmount overhead */}
|
||||||
|
<DragDropOverlay isActive={isActive} />
|
||||||
<DragDropModal
|
<DragDropModal
|
||||||
files={draggedFiles}
|
files={draggedFiles}
|
||||||
isVisible={showModal}
|
isVisible={showModal}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useDrop } from 'react-dnd';
|
import { useDrop } from 'react-dnd';
|
||||||
import { NativeTypes } from 'react-dnd-html5-backend';
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
|
Tools,
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
Constants,
|
Constants,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
|
@ -15,7 +16,6 @@ import {
|
||||||
import type { DropTargetMonitor } from 'react-dnd';
|
import type { DropTargetMonitor } from 'react-dnd';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import store, { ephemeralAgentByConvoId } from '~/store';
|
import store, { ephemeralAgentByConvoId } from '~/store';
|
||||||
import { useAgentToolPermissions } from '~/hooks';
|
|
||||||
import useFileHandling from './useFileHandling';
|
import useFileHandling from './useFileHandling';
|
||||||
|
|
||||||
export default function useDragHelpers() {
|
export default function useDragHelpers() {
|
||||||
|
|
@ -23,25 +23,10 @@ export default function useDragHelpers() {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||||
const agentId = conversation?.agent_id ?? '';
|
|
||||||
const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(agentId);
|
|
||||||
const setEphemeralAgent = useSetRecoilState(
|
const setEphemeralAgent = useSetRecoilState(
|
||||||
ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO),
|
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(
|
const isAssistants = useMemo(
|
||||||
() => isAssistantsEndpoint(conversation?.endpoint),
|
() => isAssistantsEndpoint(conversation?.endpoint),
|
||||||
[conversation?.endpoint],
|
[conversation?.endpoint],
|
||||||
|
|
@ -51,13 +36,33 @@ export default function useDragHelpers() {
|
||||||
overrideEndpoint: isAssistants ? undefined : EModelEndpoint.agents,
|
overrideEndpoint: isAssistants ? undefined : EModelEndpoint.agents,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ canDrop, isOver }, drop] = useDrop(
|
const handleOptionSelect = useCallback(
|
||||||
() => ({
|
(toolResource: EToolResources | undefined) => {
|
||||||
accept: [NativeTypes.FILE],
|
/** File search is not automatically enabled to simulate legacy behavior */
|
||||||
drop(item: { files: File[] }) {
|
if (toolResource && toolResource !== EToolResources.file_search) {
|
||||||
console.log('drop', item.files);
|
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) {
|
if (isAssistants) {
|
||||||
handleFiles(item.files);
|
handleFilesRef.current(item.files);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +73,25 @@ export default function useDragHelpers() {
|
||||||
const codeEnabled = capabilities.includes(AgentCapabilities.execute_code) === true;
|
const codeEnabled = capabilities.includes(AgentCapabilities.execute_code) === true;
|
||||||
const ocrEnabled = capabilities.includes(AgentCapabilities.ocr) === 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<t.Agent>([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) */
|
/** Determine if dragged files are all images (enables the base image option) */
|
||||||
const allImages = item.files.every((f) => f.type?.startsWith('image/'));
|
const allImages = item.files.every((f) => f.type?.startsWith('image/'));
|
||||||
|
|
||||||
|
|
@ -79,19 +103,28 @@ export default function useDragHelpers() {
|
||||||
|
|
||||||
if (!shouldShowModal) {
|
if (!shouldShowModal) {
|
||||||
// Fallback: directly handle files without showing modal
|
// Fallback: directly handle files without showing modal
|
||||||
handleFiles(item.files);
|
handleFilesRef.current(item.files);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDraggedFiles(item.files);
|
setDraggedFiles(item.files);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
},
|
},
|
||||||
|
[isAssistants, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [{ canDrop, isOver }, drop] = useDrop(
|
||||||
|
() => ({
|
||||||
|
accept: [NativeTypes.FILE],
|
||||||
|
drop: handleDrop,
|
||||||
canDrop: () => true,
|
canDrop: () => true,
|
||||||
collect: (monitor: DropTargetMonitor) => ({
|
collect: (monitor: DropTargetMonitor) => {
|
||||||
isOver: monitor.isOver(),
|
/** Optimize collect to reduce re-renders */
|
||||||
canDrop: monitor.canDrop(),
|
const isOver = monitor.isOver();
|
||||||
|
const canDrop = monitor.canDrop();
|
||||||
|
return { isOver, canDrop };
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
[handleDrop],
|
||||||
[handleFiles],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -848,7 +848,7 @@
|
||||||
"com_ui_download_backup": "Download Backup Codes",
|
"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_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_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": "Dropdown variables:",
|
||||||
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
||||||
"com_ui_duplicate": "Duplicate",
|
"com_ui_duplicate": "Duplicate",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue