mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 18:30:15 +01:00
📜 refactor: Optimize Conversation History Nav with Cursor Pagination (#5785)
* ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * ✨ feat: implement cursor-based pagination for conversations API * 🔧 refactor: remove createdAt from conversation selection in API and type definitions * 🔧 refactor: include createdAt in conversation selection and update related types * ✨ fix: search functionality and bugs with loadMoreConversations * feat: move ArchivedChats to cursor and DataTable standard * 🔧 refactor: add InfiniteQueryObserverResult type import in Nav component * feat: enhance conversation listing with pagination, sorting, and search capabilities * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: remove unused translation keys for archived chats and search results * 🔧 fix: Archived Chats, Delete Convo, Duplicate Convo * 🔧 refactor: improve conversation components with layout adjustments and new translations * 🔧 refactor: simplify archive conversation mutation and improve unarchive handling; fix: update fork mutation * 🔧 refactor: decode search query parameter in conversation route; improve error handling in unarchive mutation; clean up DataTable component styles * 🔧 refactor: remove unused translation key for empty archived chats * 🚀 fix: `archivedConversation` query key not updated correctly while archiving * 🧠 feat: Bedrock Anthropic Reasoning & Update Endpoint Handling (#6163) * feat: Add thinking and thinkingBudget parameters for Bedrock Anthropic models * chore: Update @librechat/agents to version 2.1.8 * refactor: change region order in params * refactor: Add maxTokens parameter to conversation preset schema * refactor: Update agent client to use bedrockInputSchema and improve error handling for model parameters * refactor: streamline/optimize llmConfig initialization and saving for bedrock * fix: ensure config titleModel is used for all endpoints * refactor: enhance OpenAIClient and agent initialization to support endpoint checks for OpenRouter * chore: bump @google/generative-ai * ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: update translation keys for clarity; simplify conversation query parameters and improve sorting functionality in SharedLinks component * 🔧 refactor: optimize conversation loading logic and improve search handling in Nav component * fix: package-lock * fix: package-lock 2 * fix: package lock 3 * refactor: remove unused utility files and exports to clean up the codebase * refactor: remove i18n and useAuthRedirect modules to streamline codebase * refactor: optimize Conversations component and remove unused ToggleContext * refactor(Convo): add RenameForm and ConvoLink components; enhance Conversations component with responsive design * fix: add missing @azure/storage-blob dependency in package.json * refactor(Search): add error handling with toast notification for search errors * refactor: make createdAt and updatedAt fields of tConvoUpdateSchema less restrictive if timestamps are missing * chore: update @azure/storage-blob dependency to version 12.27.0, ensure package-lock is correct * refactor(Search): improve conversation handling server side * fix: eslint warning and errors * refactor(Search): improved search loading state and overall UX * Refactors conversation cache management Centralizes conversation mutation logic into dedicated utility functions for adding, updating, and removing conversations from query caches. Improves reliability and maintainability by: - Consolidating duplicate cache manipulation code - Adding type safety for infinite query data structures - Implementing consistent cache update patterns across all conversation operations - Removing obsolete conversation helper functions in favor of standardized utilities * fix: conversation handling and SSE event processing - Optimizes conversation state management with useMemo and proper hook ordering - Improves SSE event handler documentation and error handling - Adds reset guard flag for conversation changes - Removes redundant navigation call - Cleans up cursor handling logic and document structure Improves code maintainability and prevents potential race conditions in conversation state updates * refactor: add type for SearchBar `onChange` * fix: type tags * style: rounded to xl all Header buttons * fix: activeConvo in Convo not working * style(Bookmarks): improved UI * a11y(AccountSettings): fixed hover style not visible when using light theme * style(SettingsTabs): improved tab switchers and dropdowns * feat: add translations keys for Speech * chore: fix package-lock * fix(mutations): legacy import after rebase * feat: refactor conversation navigation for accessibility * fix(search): convo and message create/update date not returned * fix(search): show correct iconURL and endpoint for searched messages * fix: small UI improvements * chore: console.log cleanup * chore: fix tests * fix(ChatForm): improve conversation ID handling and clean up useMemo dependencies * chore: improve typing * chore: improve typing * fix(useSSE): clear conversation ID on submission to prevent draft restoration * refactor(OpenAIClient): clean up abort handler * refactor(abortMiddleware): change handleAbort to use function expression * feat: add PENDING_CONVO constant and update conversation ID checks * fix: final event handling on abort * fix: improve title sync and query cache sync on final event * fix: prevent overwriting cached conversation data if it already exists --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
77a21719fd
commit
650e9b4f6c
69 changed files with 3434 additions and 2139 deletions
|
|
@ -1,28 +1,26 @@
|
|||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks';
|
||||
import { useUpdateConversationMutation } from '~/data-provider';
|
||||
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { ConvoOptions } from './ConvoOptions';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import RenameForm from './RenameForm';
|
||||
import ConvoLink from './ConvoLink';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
type KeyEvent = KeyboardEvent<HTMLInputElement>;
|
||||
|
||||
type ConversationProps = {
|
||||
interface ConversationProps {
|
||||
conversation: TConversation;
|
||||
retainView: () => void;
|
||||
toggleNav: () => void;
|
||||
isLatestConvo: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Conversation({
|
||||
conversation,
|
||||
|
|
@ -31,27 +29,81 @@ export default function Conversation({
|
|||
isLatestConvo,
|
||||
}: ConversationProps) {
|
||||
const params = useParams();
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
||||
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const { showToast } = useToastContext();
|
||||
const { conversationId, title } = conversation;
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [titleInput, setTitleInput] = useState(title);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { conversationId, title = '' } = conversation;
|
||||
|
||||
const [titleInput, setTitleInput] = useState(title || '');
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const localize = useLocalize();
|
||||
|
||||
const clickHandler = async (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (event.button === 0 && (event.ctrlKey || event.metaKey)) {
|
||||
toggleNav();
|
||||
const previousTitle = useRef(title);
|
||||
|
||||
useEffect(() => {
|
||||
if (title !== previousTitle.current) {
|
||||
setTitleInput(title as string);
|
||||
previousTitle.current = title;
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
const isActiveConvo = useMemo(() => {
|
||||
if (conversationId === Constants.NEW_CONVO) {
|
||||
return currentConvoId === Constants.NEW_CONVO;
|
||||
}
|
||||
|
||||
if (currentConvoId !== Constants.NEW_CONVO) {
|
||||
return currentConvoId === conversationId;
|
||||
} else {
|
||||
const latestConvo = activeConvos?.[0];
|
||||
return latestConvo === conversationId;
|
||||
}
|
||||
}, [currentConvoId, conversationId, activeConvos]);
|
||||
|
||||
const handleRename = () => {
|
||||
setIsPopoverActive(false);
|
||||
setTitleInput(title as string);
|
||||
setRenaming(true);
|
||||
};
|
||||
|
||||
const handleRenameSubmit = async (newTitle: string) => {
|
||||
if (!conversationId || newTitle === title) {
|
||||
setRenaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
try {
|
||||
await updateConvoMutation.mutateAsync({
|
||||
conversationId,
|
||||
title: newTitle.trim() || localize('com_ui_untitled'),
|
||||
});
|
||||
setRenaming(false);
|
||||
} catch (error) {
|
||||
setTitleInput(title as string);
|
||||
showToast({
|
||||
message: localize('com_ui_rename_failed'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
setRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setTitleInput(title as string);
|
||||
setRenaming(false);
|
||||
};
|
||||
|
||||
const handleNavigation = (ctrlOrMetaKey: boolean) => {
|
||||
if (ctrlOrMetaKey) {
|
||||
toggleNav();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentConvoId === conversationId || isPopoverActive) {
|
||||
return;
|
||||
|
|
@ -59,138 +111,68 @@ export default function Conversation({
|
|||
|
||||
toggleNav();
|
||||
|
||||
// set document title
|
||||
if (typeof title === 'string' && title.length > 0) {
|
||||
document.title = title;
|
||||
}
|
||||
/* Note: Latest Message should not be reset if existing convo */
|
||||
|
||||
navigateWithLastTools(
|
||||
conversation,
|
||||
!(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
);
|
||||
};
|
||||
|
||||
const renameHandler = useCallback(() => {
|
||||
setIsPopoverActive(false);
|
||||
setTitleInput(title);
|
||||
setRenaming(true);
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renaming && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [renaming]);
|
||||
|
||||
const onRename = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
|
||||
e.preventDefault();
|
||||
setRenaming(false);
|
||||
if (titleInput === title) {
|
||||
return;
|
||||
}
|
||||
if (typeof conversationId !== 'string' || conversationId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConvoMutation.mutate(
|
||||
{ conversationId, title: titleInput ?? '' },
|
||||
{
|
||||
onError: () => {
|
||||
setTitleInput(title);
|
||||
showToast({
|
||||
message: 'Failed to rename conversation',
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
[title, titleInput, conversationId, showToast, updateConvoMutation],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setTitleInput(title);
|
||||
setRenaming(false);
|
||||
} else if (e.key === 'Enter') {
|
||||
onRename(e);
|
||||
}
|
||||
},
|
||||
[title, onRename],
|
||||
);
|
||||
|
||||
const cancelRename = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setTitleInput(title);
|
||||
setRenaming(false);
|
||||
},
|
||||
[title],
|
||||
);
|
||||
|
||||
const isActiveConvo: boolean = useMemo(
|
||||
() =>
|
||||
currentConvoId === conversationId ||
|
||||
(isLatestConvo &&
|
||||
currentConvoId === 'new' &&
|
||||
activeConvos[0] != null &&
|
||||
activeConvos[0] !== 'new'),
|
||||
[currentConvoId, conversationId, isLatestConvo, activeConvos],
|
||||
);
|
||||
const convoOptionsProps = {
|
||||
title,
|
||||
retainView,
|
||||
renameHandler: handleRename,
|
||||
isActiveConvo,
|
||||
conversationId,
|
||||
isPopoverActive,
|
||||
setIsPopoverActive,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative mt-2 flex h-9 w-full items-center rounded-lg hover:bg-surface-active-alt',
|
||||
isActiveConvo ? 'bg-surface-active-alt' : '',
|
||||
isSmallScreen ? 'h-12' : '',
|
||||
'group relative flex h-12 w-full items-center rounded-lg transition-colors duration-200 md:h-9',
|
||||
isActiveConvo ? 'bg-surface-active-alt' : 'hover:bg-surface-active-alt',
|
||||
)}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
if (renaming) {
|
||||
return;
|
||||
}
|
||||
if (e.button === 0) {
|
||||
handleNavigation(e.ctrlKey || e.metaKey);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (renaming) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
handleNavigation(false);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: renaming ? 'default' : 'pointer' }}
|
||||
data-testid="convo-item"
|
||||
>
|
||||
{renaming ? (
|
||||
<div className="absolute inset-0 z-20 flex w-full items-center rounded-lg bg-surface-active-alt p-1.5">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight focus-visible:outline-none"
|
||||
value={titleInput ?? ''}
|
||||
onChange={(e) => setTitleInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={`${localize('com_ui_rename')} ${localize('com_ui_chat')}`}
|
||||
/>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={cancelRename}
|
||||
aria-label={`${localize('com_ui_cancel')} ${localize('com_ui_rename')}`}
|
||||
>
|
||||
<X
|
||||
aria-hidden={true}
|
||||
className="h-4 w-4 transition-colors duration-200 ease-in-out hover:opacity-70"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={onRename}
|
||||
aria-label={`${localize('com_ui_submit')} ${localize('com_ui_rename')}`}
|
||||
>
|
||||
<Check
|
||||
aria-hidden={true}
|
||||
className="h-4 w-4 transition-colors duration-200 ease-in-out hover:opacity-70"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RenameForm
|
||||
titleInput={titleInput}
|
||||
setTitleInput={setTitleInput}
|
||||
onSubmit={handleRenameSubmit}
|
||||
onCancel={handleCancelRename}
|
||||
localize={localize}
|
||||
/>
|
||||
) : (
|
||||
<a
|
||||
href={`/c/${conversationId}`}
|
||||
data-testid="convo-item"
|
||||
onClick={clickHandler}
|
||||
className={cn(
|
||||
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
|
||||
isActiveConvo ? 'bg-surface-active-alt' : '',
|
||||
)}
|
||||
title={title ?? ''}
|
||||
<ConvoLink
|
||||
isActiveConvo={isActiveConvo}
|
||||
title={title}
|
||||
onRename={handleRename}
|
||||
isSmallScreen={isSmallScreen}
|
||||
localize={localize}
|
||||
>
|
||||
<EndpointIcon
|
||||
conversation={conversation}
|
||||
|
|
@ -198,23 +180,7 @@ export default function Conversation({
|
|||
size={20}
|
||||
context="menu-item"
|
||||
/>
|
||||
<div
|
||||
className="relative line-clamp-1 flex-1 grow overflow-hidden"
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTitleInput(title);
|
||||
setRenaming(true);
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{isActiveConvo ? (
|
||||
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l" />
|
||||
) : (
|
||||
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-surface-primary-alt from-0% to-transparent group-hover:from-surface-active-alt group-hover:from-40%" />
|
||||
)}
|
||||
</a>
|
||||
</ConvoLink>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -224,17 +190,7 @@ export default function Conversation({
|
|||
: 'hidden group-focus-within:flex group-hover:flex',
|
||||
)}
|
||||
>
|
||||
{!renaming && (
|
||||
<ConvoOptions
|
||||
title={title}
|
||||
retainView={retainView}
|
||||
renameHandler={renameHandler}
|
||||
isActiveConvo={isActiveConvo}
|
||||
conversationId={conversationId}
|
||||
isPopoverActive={isPopoverActive}
|
||||
setIsPopoverActive={setIsPopoverActive}
|
||||
/>
|
||||
)}
|
||||
{!renaming && <ConvoOptions {...convoOptionsProps} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue