mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
⌨️ feat: Add Shift-Key Shortcuts for Instant Conversation Actions (#10732)
* 🪦 refactor: Remove Legacy Code (#10533) * 🗑️ chore: Remove unused Legacy Provider clients and related helpers * Deleted OpenAIClient and GoogleClient files along with their associated tests. * Removed references to these clients in the clients index file. * Cleaned up typedefs by removing the OpenAISpecClient export. * Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes. * chore/remove-openapi-specs * 🗑️ chore: Remove unused mergeSort and misc utility functions * Deleted mergeSort.js and misc.js files as they are no longer needed. * Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic. * Updated mongoMeili.ts to eliminate local implementations of removed functions. * chore: remove legacy endpoints * chore: remove all plugins endpoint related code * chore: remove unused prompt handling code and clean up imports * Deleted handleInputs.js and instructions.js files as they are no longer needed. * Removed references to these files in the prompts index.js. * Updated docker-compose.yml to simplify reverse proxy configuration. * chore: remove unused LightningIcon import from Icons.tsx * chore: clean up translation.json by removing deprecated and unused keys * chore: update Jest configuration and remove unused mock file * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock. * Deleted the fetchEventSource.js mock file as it is no longer needed. * fix: simplify endpoint type check in Landing and ConversationStarters components * Updated the endpoint type check to use strict equality for better clarity and performance. * Ensured consistency in the handling of the azureOpenAI endpoint across both components. * chore: remove unused dependencies from package.json and package-lock.json * chore: remove legacy EditController, associated routes and imports * chore: update banResponse logic to refine request handling for banned users * chore: remove unused validateEndpoint middleware and its references * chore: remove unused 'res' parameter from initializeClient in multiple endpoint files * chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions * chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import * chore: remove unused TResPlugin type and related references; clean up imports in types and schemas * 📦 chore: Bump Express.js to v5 (#10671) * chore: update express to version 5.1.0 in package.json * chore: update express-rate-limit to version 8.2.1 in package.json and package-lock.json * fix: Enhance server startup error handling in experimental and index files * Added error handling for server startup in both experimental.js and index.js to log errors and exit the process if the server fails to start. * Updated comments in openidStrategy.js to clarify the purpose of the CustomOpenIDStrategy class and its relation to Express version changes. * chore: Implement rate limiting for all POST routes excluding /speech, required for express v5 * Added middleware to apply IP and user rate limiters to all POST requests, ensuring that the /speech route remains unaffected. * Enhanced code clarity with comments explaining the new rate limiting logic. * chore: Enable writable req.query for mongoSanitize compatibility in Express 5 * chore: Ensure req.body exists in multiple middleware and route files for Express 5 compatibility * 🪨 feat: Add PROXY support for AWS Bedrock endpoints (#8871) * feat: added PROXY support for AWS Bedrock endpoint * chore: explicit install of new packages required for bedrock proxy --------- Co-authored-by: Danny Avila <danny@librechat.ai> * feat: add shift key tracking and instant delete functionality in conversation options * refactor(Convo): simplify classname logic * fix: restore package-lock after rebase --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: Arthur Barrett <abarrett@fas.harvard.edu>
This commit is contained in:
parent
470a73b406
commit
41c0a96d39
4 changed files with 176 additions and 43 deletions
|
|
@ -6,7 +6,7 @@ import { useToastContext, useMediaQuery } from '@librechat/client';
|
|||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useUpdateConversationMutation } from '~/data-provider';
|
||||
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
||||
import { useNavigateToConvo, useLocalize } from '~/hooks';
|
||||
import { useNavigateToConvo, useLocalize, useShiftKey } from '~/hooks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { ConvoOptions } from './ConvoOptions';
|
||||
|
|
@ -31,6 +31,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
|||
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const isShiftHeld = useShiftKey();
|
||||
const { conversationId, title = '' } = conversation;
|
||||
|
||||
const [titleInput, setTitleInput] = useState(title || '');
|
||||
|
|
@ -191,8 +192,9 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
|||
className={cn(
|
||||
'mr-2 flex origin-left',
|
||||
isPopoverActive || isActiveConvo
|
||||
? 'pointer-events-auto max-w-[28px] scale-x-100 opacity-100'
|
||||
: 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[28px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[28px] group-hover:scale-x-100 group-hover:opacity-100',
|
||||
? 'pointer-events-auto scale-x-100 opacity-100'
|
||||
: 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[60px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[60px] group-hover:scale-x-100 group-hover:opacity-100',
|
||||
(isPopoverActive || isActiveConvo) && (isShiftHeld ? 'max-w-[60px]' : 'max-w-[28px]'),
|
||||
)}
|
||||
// Removing aria-hidden to fix accessibility issue: ARIA hidden element must not be focusable or contain focusable elements
|
||||
// but not sure what its original purpose was, so leaving the property commented out until it can be cleared safe to delete.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { useState, useId, useRef, memo, useCallback, useMemo } from 'react';
|
||||
import * as Menu from '@ariakit/react/menu';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { DropdownPopup, Spinner, useToastContext } from '@librechat/client';
|
||||
import { Ellipsis, Share2, CopyPlus, Archive, Pen, Trash } from 'lucide-react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import {
|
||||
useDuplicateConversationMutation,
|
||||
useDeleteConversationMutation,
|
||||
useGetStartupConfig,
|
||||
useArchiveConvoMutation,
|
||||
} from '~/data-provider';
|
||||
import { useLocalize, useNavigateToConvo, useNewConvo } from '~/hooks';
|
||||
import { useLocalize, useNavigateToConvo, useNewConvo, useShiftKey } from '~/hooks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import DeleteButton from './DeleteButton';
|
||||
|
|
@ -34,6 +38,8 @@ function ConvoOptions({
|
|||
isActiveConvo: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const isShiftHeld = useShiftKey();
|
||||
const { index } = useChatContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { navigateToConvo } = useNavigateToConvo(index);
|
||||
|
|
@ -51,6 +57,28 @@ function ConvoOptions({
|
|||
|
||||
const archiveConvoMutation = useArchiveConvoMutation();
|
||||
|
||||
const deleteMutation = useDeleteConversationMutation({
|
||||
onSuccess: () => {
|
||||
if (currentConvoId === conversationId || currentConvoId === 'new') {
|
||||
newConversation();
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
retainView();
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateConversation = useDuplicateConversationMutation({
|
||||
onSuccess: (data) => {
|
||||
navigateToConvo(data.conversation);
|
||||
|
|
@ -76,6 +104,7 @@ function ConvoOptions({
|
|||
|
||||
const isDuplicateLoading = duplicateConversation.isLoading;
|
||||
const isArchiveLoading = archiveConvoMutation.isLoading;
|
||||
const isDeleteLoading = deleteMutation.isLoading;
|
||||
|
||||
const handleShareClick = useCallback(() => {
|
||||
setShowShareDialog(true);
|
||||
|
|
@ -85,47 +114,70 @@ function ConvoOptions({
|
|||
setShowDeleteDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleArchiveClick = useCallback(async () => {
|
||||
const convoId = conversationId ?? '';
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
const handleInstantDelete = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const convoId = conversationId ?? '';
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
archiveConvoMutation.mutate(
|
||||
{ conversationId: convoId, isArchived: true },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAnnouncement(localize('com_ui_convo_archived'));
|
||||
setTimeout(() => {
|
||||
setAnnouncement('');
|
||||
}, 10000);
|
||||
if (currentConvoId === convoId || currentConvoId === 'new') {
|
||||
newConversation();
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
retainView();
|
||||
setIsPopoverActive(false);
|
||||
const messages = queryClient.getQueryData<TMessage[]>([QueryKeys.messages, convoId]);
|
||||
const thread_id = messages?.[messages.length - 1]?.thread_id;
|
||||
const endpoint = messages?.[messages.length - 1]?.endpoint;
|
||||
|
||||
deleteMutation.mutate({ conversationId: convoId, thread_id, endpoint, source: 'button' });
|
||||
},
|
||||
[conversationId, deleteMutation, queryClient],
|
||||
);
|
||||
|
||||
const handleArchiveClick = useCallback(
|
||||
async (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
const convoId = conversationId ?? '';
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
archiveConvoMutation.mutate(
|
||||
{ conversationId: convoId, isArchived: true },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAnnouncement(localize('com_ui_convo_archived'));
|
||||
setTimeout(() => {
|
||||
setAnnouncement('');
|
||||
}, 10000);
|
||||
|
||||
if (currentConvoId === convoId || currentConvoId === 'new') {
|
||||
newConversation();
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
|
||||
retainView();
|
||||
setIsPopoverActive(false);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_archive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_archive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [
|
||||
conversationId,
|
||||
currentConvoId,
|
||||
archiveConvoMutation,
|
||||
navigate,
|
||||
newConversation,
|
||||
retainView,
|
||||
setIsPopoverActive,
|
||||
showToast,
|
||||
localize,
|
||||
]);
|
||||
);
|
||||
},
|
||||
[
|
||||
conversationId,
|
||||
currentConvoId,
|
||||
archiveConvoMutation,
|
||||
navigate,
|
||||
newConversation,
|
||||
retainView,
|
||||
setIsPopoverActive,
|
||||
showToast,
|
||||
localize,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
duplicateConversation.mutate({
|
||||
|
|
@ -195,6 +247,44 @@ function ConvoOptions({
|
|||
|
||||
const menuId = useId();
|
||||
|
||||
const buttonClassName = cn(
|
||||
'inline-flex h-7 w-7 items-center justify-center rounded-md border-none p-0 text-sm font-medium ring-ring-primary transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50',
|
||||
isActiveConvo === true || isPopoverActive
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
|
||||
);
|
||||
|
||||
if (isShiftHeld) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
aria-label={localize('com_ui_archive')}
|
||||
className={cn(buttonClassName, 'hover:bg-surface-hover')}
|
||||
onClick={handleArchiveClick}
|
||||
disabled={isArchiveLoading}
|
||||
>
|
||||
{isArchiveLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Archive className="icon-md text-text-secondary" aria-hidden={true} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
aria-label={localize('com_ui_delete')}
|
||||
className={cn(buttonClassName, 'hover:bg-surface-hover')}
|
||||
onClick={handleInstantDelete}
|
||||
disabled={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Trash className="icon-md text-text-secondary" aria-hidden={true} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="sr-only" aria-live="polite" aria-atomic="true">
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './useLazyEffect';
|
||||
export { default as useShiftKey } from './useShiftKey';
|
||||
|
|
|
|||
40
client/src/hooks/Generic/useShiftKey.ts
Normal file
40
client/src/hooks/Generic/useShiftKey.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Hook to track whether the shift key is currently being held down
|
||||
* @returns boolean indicating if shift key is pressed
|
||||
*/
|
||||
export default function useShiftKey(): boolean {
|
||||
const [isShiftHeld, setIsShiftHeld] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftHeld(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftHeld(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset shift state when window loses focus
|
||||
const handleBlur = () => {
|
||||
setIsShiftHeld(false);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isShiftHeld;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue