📌 feat: Pin Agents and Models in the Sidebar (#10634)

* 🪦 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

* 🪦 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: MCP Status Accessibility Improvements (#10738)

* feat: make MultiSelect highlight same opacity as other focus highlights in app

* feat: add better screenreader announcements for mcp server and variable states

* feat: memoize fullTitle calculation

* 🪨 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: Implement Favorites functionality with controllers, hooks, and UI components

*  feat: Refactor Favorites functionality to support new data structure and enhance UI interactions

*  feat: Add endpoint to new conversation for agent favorites

*  feat: Enhance Conversations and Favorites components with expanded functionality and improved UI interactions

*  feat: Remove 'Pinned' label from UI translations for cleaner interface

* feat: clean up comments and improve code readability in favorites and agent components; bump @librechat/data-schemas to 0.0.24

*  feat: Enhance favorites management with validation, update data structure, and improve UI interactions

*  feat: Simplify rendering logic in EndpointModelItem and optimize useEffect dependencies in Conversations component

*  test: Update favorites mock implementation and improve button focus styles in AgentDetail tests

*  feat: Enhance favorites management by adding loading and error states, and refactor related hooks and components

*  feat: Add loading skeletons for favorites while agents are being fetched

*  feat: Improve loading experience in FavoritesList by adding skeleton placeholders for favorites and marketplace

* feat: Optimize cache handling in Conversations and enhance FavoritesList to notify height changes on loading completion

*  feat: Add loading skeleton for SearchBar in Nav component and update agent avatar fallback icon to Feather

* feat: Refactor FavoritesController validation, streamline ModelSelector component, and enhance EndpointModelItem with selection state

* feat: Adjust padding in Conversations and FavoritesList components for improved layout consistency

* feat: Refactor FavoritesController to use model methods for user updates and retrieval

* feat: Enhance Favorites functionality with validation, cleanup, and improved error handling

* tests: Update AgentCard and agent utilities to use Feather icon fallback instead of Bot icon

* refactor: Remove collapsible animation styles from CSS

* feat: Migrate favorites state management from Recoil to Jotai

* fix: Correct type definition in useGetFavoritesQuery and ensure useFavorites is exported

* refactor: Simplify AuthField component by removing TooltipAnchor and directly rendering Label

* fix: Ensure favorites are always an array and update references in FavoritesList

* style: Update Conversation component styles for improved UI consistency

* feat: re-integrate AuthContext to manage agent marketplace visibility based on authentication state

* fix: Improve optimistic updates in favorites mutation handling

* feat: Implement error handling for favorites limit and consolidate marketplace access logic

* fix: package-lock

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
Co-authored-by: Arthur Barrett <abarrett@fas.harvard.edu>
This commit is contained in:
Marco Beretta 2025-12-04 20:41:52 +01:00 committed by Danny Avila
parent cea4f57a73
commit b6e5ea5d33
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
31 changed files with 1310 additions and 184 deletions

View file

@ -1,9 +1,8 @@
import React, { useCallback, useContext } from 'react';
import React, { useCallback } from 'react';
import { LayoutGrid } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { TooltipAnchor, Button } from '@librechat/client';
import { useLocalize, useHasAccess, AuthContext } from '~/hooks';
import { useLocalize, useShowMarketplace } from '~/hooks';
interface AgentMarketplaceButtonProps {
isSmallScreen?: boolean;
@ -16,17 +15,7 @@ export default function AgentMarketplaceButton({
}: AgentMarketplaceButtonProps) {
const navigate = useNavigate();
const localize = useLocalize();
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
const showAgentMarketplace = useShowMarketplace();
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');
@ -35,14 +24,6 @@ export default function AgentMarketplaceButton({
}
}, [navigate, isSmallScreen, toggleNav]);
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
if (!showAgentMarketplace) {
return null;
}

View file

@ -0,0 +1,150 @@
import React, { useState } from 'react';
import * as Menu from '@ariakit/react/menu';
import { useNavigate } from 'react-router-dom';
import { Ellipsis, PinOff } from 'lucide-react';
import { DropdownPopup } from '@librechat/client';
import { EModelEndpoint } from 'librechat-data-provider';
import type { FavoriteModel } from '~/store/favorites';
import type t from 'librechat-data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { useNewConvo, useFavorites, useLocalize } from '~/hooks';
import { renderAgentAvatar, cn } from '~/utils';
type FavoriteItemProps = {
item: t.Agent | FavoriteModel;
type: 'agent' | 'model';
};
export default function FavoriteItem({ item, type }: FavoriteItemProps) {
const navigate = useNavigate();
const localize = useLocalize();
const { newConversation } = useNewConvo();
const { removeFavoriteAgent, removeFavoriteModel } = useFavorites();
const [isPopoverActive, setIsPopoverActive] = useState(false);
const handleClick = (e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest('[data-testid="favorite-options-button"]')) {
return;
}
if (type === 'agent') {
const agent = item as t.Agent;
newConversation({
template: {
...agent,
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
},
preset: {
...agent,
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
},
});
navigate(`/c/new`);
} else {
const model = item as FavoriteModel;
newConversation({
template: {
endpoint: model.endpoint,
model: model.model,
},
preset: {
endpoint: model.endpoint,
model: model.model,
},
});
navigate(`/c/new`);
}
};
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation();
if (type === 'agent') {
removeFavoriteAgent((item as t.Agent).id);
} else {
const model = item as FavoriteModel;
removeFavoriteModel(model.model, model.endpoint);
}
setIsPopoverActive(false);
};
const renderIcon = () => {
if (type === 'agent') {
return renderAgentAvatar(item as t.Agent, { size: 'icon', className: 'mr-2' });
}
const model = item as FavoriteModel;
return (
<div className="mr-2 h-5 w-5">
<EndpointIcon
conversation={{ endpoint: model.endpoint, model: model.model } as t.TConversation}
endpoint={model.endpoint}
model={model.model}
size={20}
/>
</div>
);
};
const getName = () => {
if (type === 'agent') {
return (item as t.Agent).name;
}
return (item as FavoriteModel).model;
};
const menuId = React.useId();
const dropdownItems = [
{
label: localize('com_ui_unpin'),
onClick: handleRemove,
icon: <PinOff className="h-4 w-4 text-text-secondary" />,
},
];
return (
<div
className={cn(
'group relative flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm text-text-primary hover:bg-surface-active-alt',
isPopoverActive ? 'bg-surface-active-alt' : '',
)}
onClick={handleClick}
data-testid="favorite-item"
>
<div className="flex flex-1 items-center truncate pr-6">
{renderIcon()}
<span className="truncate">{getName()}</span>
</div>
<div
className={cn(
'absolute right-2 flex items-center',
isPopoverActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
)}
onClick={(e) => e.stopPropagation()}
>
<DropdownPopup
portal={true}
mountByState={true}
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
<Menu.MenuButton
className={cn(
'flex h-7 w-7 items-center justify-center rounded-md',
isPopoverActive ? 'bg-surface-active-alt' : '',
)}
aria-label={localize('com_ui_options')}
data-testid="favorite-options-button"
>
<Ellipsis className="h-4 w-4 text-text-secondary" />
</Menu.MenuButton>
}
items={dropdownItems}
menuId={menuId}
/>
</div>
</div>
);
}

View file

@ -0,0 +1,299 @@
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { LayoutGrid } from 'lucide-react';
import { useDrag, useDrop } from 'react-dnd';
import { Skeleton } from '@librechat/client';
import { useNavigate } from 'react-router-dom';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQueries, useQueryClient } from '@tanstack/react-query';
import type { InfiniteData } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { useFavorites, useLocalize, useShowMarketplace } from '~/hooks';
import FavoriteItem from './FavoriteItem';
import store from '~/store';
const FavoriteItemSkeleton = () => (
<div className="flex w-full items-center rounded-lg px-3 py-2">
<Skeleton className="mr-2 h-5 w-5 rounded-full" />
<Skeleton className="h-4 w-24" />
</div>
);
const MarketplaceSkeleton = () => (
<div className="flex w-full items-center rounded-lg px-3 py-2">
<Skeleton className="mr-2 h-5 w-5" />
<Skeleton className="h-4 w-28" />
</div>
);
interface DraggableFavoriteItemProps {
id: string;
index: number;
moveItem: (dragIndex: number, hoverIndex: number) => void;
onDrop: () => void;
children: React.ReactNode;
}
const DraggableFavoriteItem = ({
id,
index,
moveItem,
onDrop,
children,
}: DraggableFavoriteItemProps) => {
const ref = useRef<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<{ index: number; id: string }, unknown, { handlerId: any }>(
{
accept: 'favorite-item',
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
if (!clientOffset) {
return;
}
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveItem(dragIndex, hoverIndex);
item.index = hoverIndex;
},
},
);
const [{ isDragging }, drag] = useDrag({
type: 'favorite-item',
item: () => {
return { id, index };
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: () => {
onDrop();
},
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
{children}
</div>
);
};
export default function FavoritesList({
isSmallScreen,
toggleNav,
onHeightChange,
}: {
isSmallScreen?: boolean;
toggleNav?: () => void;
/** Callback when the list height might have changed (e.g., agents finished loading) */
onHeightChange?: () => void;
}) {
const navigate = useNavigate();
const localize = useLocalize();
const queryClient = useQueryClient();
const search = useRecoilValue(store.search);
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
const showAgentMarketplace = useShowMarketplace();
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');
if (isSmallScreen && toggleNav) {
toggleNav();
}
}, [navigate, isSmallScreen, toggleNav]);
// Ensure favorites is always an array (could be corrupted in localStorage)
const safeFavorites = useMemo(() => (Array.isArray(favorites) ? favorites : []), [favorites]);
const agentIds = safeFavorites.map((f) => f.agentId).filter(Boolean) as string[];
const agentQueries = useQueries({
queries: agentIds.map((agentId) => ({
queryKey: [QueryKeys.agent, agentId],
queryFn: () => dataService.getAgentById({ agent_id: agentId }),
staleTime: 1000 * 60 * 5,
})),
});
const isAgentsLoading = agentIds.length > 0 && agentQueries.some((q) => q.isLoading);
useEffect(() => {
if (!isAgentsLoading && onHeightChange) {
onHeightChange();
}
}, [isAgentsLoading, onHeightChange]);
const agentsMap = useMemo(() => {
const map: Record<string, t.Agent> = {};
const addToMap = (agent: t.Agent) => {
if (agent && agent.id && !map[agent.id]) {
map[agent.id] = agent;
}
};
const marketplaceData = queryClient.getQueriesData<InfiniteData<t.AgentListResponse>>([
QueryKeys.marketplaceAgents,
]);
marketplaceData.forEach(([_, data]) => {
data?.pages.forEach((page) => {
page.data.forEach(addToMap);
});
});
const agentsListData = queryClient.getQueriesData<t.AgentListResponse>([QueryKeys.agents]);
agentsListData.forEach(([_, data]) => {
if (data && Array.isArray(data.data)) {
data.data.forEach(addToMap);
}
});
agentQueries.forEach((query) => {
if (query.data) {
map[query.data.id] = query.data;
}
});
return map;
}, [agentQueries, queryClient]);
const draggedFavoritesRef = useRef(safeFavorites);
const moveItem = useCallback(
(dragIndex: number, hoverIndex: number) => {
const newFavorites = [...draggedFavoritesRef.current];
const [draggedItem] = newFavorites.splice(dragIndex, 1);
newFavorites.splice(hoverIndex, 0, draggedItem);
draggedFavoritesRef.current = newFavorites;
reorderFavorites(newFavorites, false);
},
[reorderFavorites],
);
const handleDrop = useCallback(() => {
// Persist the final order using the ref which has the latest state
reorderFavorites(draggedFavoritesRef.current, true);
}, [reorderFavorites]);
// Keep ref in sync when favorites change from external sources
useEffect(() => {
draggedFavoritesRef.current = safeFavorites;
}, [safeFavorites]);
if (search.query) {
return null;
}
if (!isFavoritesLoading && safeFavorites.length === 0 && !showAgentMarketplace) {
return null;
}
if (isFavoritesLoading) {
return (
<div className="mb-2 flex flex-col pb-2">
<div className="mt-1 flex flex-col gap-1">
{showAgentMarketplace && <MarketplaceSkeleton />}
<FavoriteItemSkeleton />
</div>
</div>
);
}
return (
<div className="mb-2 flex flex-col">
<div className="mt-1 flex flex-col gap-1">
{/* Show skeletons for ALL items while agents are still loading */}
{isAgentsLoading ? (
<>
{/* Marketplace skeleton */}
{showAgentMarketplace && <MarketplaceSkeleton />}
{/* Favorite items skeletons */}
{safeFavorites.map((_, index) => (
<FavoriteItemSkeleton key={`skeleton-${index}`} />
))}
</>
) : (
<>
{/* Agent Marketplace button */}
{showAgentMarketplace && (
<div
className="group relative flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm text-text-primary hover:bg-surface-active-alt"
onClick={handleAgentMarketplace}
data-testid="nav-agents-marketplace-button"
>
<div className="flex flex-1 items-center truncate pr-6">
<div className="mr-2 h-5 w-5">
<LayoutGrid className="h-5 w-5 text-text-primary" />
</div>
<span className="truncate">{localize('com_agents_marketplace')}</span>
</div>
</div>
)}
{safeFavorites.map((fav, index) => {
if (fav.agentId) {
const agent = agentsMap[fav.agentId];
if (!agent) {
return null;
}
return (
<DraggableFavoriteItem
key={fav.agentId}
id={fav.agentId}
index={index}
moveItem={moveItem}
onDrop={handleDrop}
>
<FavoriteItem item={agent} type="agent" />
</DraggableFavoriteItem>
);
} else if (fav.model && fav.endpoint) {
return (
<DraggableFavoriteItem
key={`${fav.endpoint}-${fav.model}`}
id={`${fav.endpoint}-${fav.model}`}
index={index}
moveItem={moveItem}
onDrop={handleDrop}
>
<FavoriteItem
item={{ model: fav.model, endpoint: fav.endpoint }}
type="model"
/>
</DraggableFavoriteItem>
);
}
return null;
})}
</>
)}
</div>
</div>
);
}

View file

@ -1,7 +1,8 @@
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { List } from 'react-virtualized';
import { AnimatePresence, motion } from 'framer-motion';
import { useMediaQuery } from '@librechat/client';
import { Skeleton, useMediaQuery } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { ConversationListResponse } from 'librechat-data-provider';
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
@ -21,11 +22,18 @@ import store from '~/store';
const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav'));
const AccountSettings = lazy(() => import('./AccountSettings'));
const AgentMarketplaceButton = lazy(() => import('./AgentMarketplaceButton'));
const NAV_WIDTH_DESKTOP = '260px';
const NAV_WIDTH_MOBILE = '320px';
const SearchBarSkeleton = memo(() => (
<div className={cn('flex h-10 items-center py-2')}>
<Skeleton className="h-10 w-full rounded-lg" />
</div>
));
SearchBarSkeleton.displayName = 'SearchBarSkeleton';
const NavMask = memo(
({ navVisible, toggleNavVisible }: { navVisible: boolean; toggleNavVisible: () => void }) => (
<div
@ -60,6 +68,7 @@ const Nav = memo(
const [navWidth, setNavWidth] = useState(NAV_WIDTH_DESKTOP);
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const [newUser, setNewUser] = useLocalStorage('newUser', true);
const [isChatsExpanded, setIsChatsExpanded] = useLocalStorage('chatsExpanded', true);
const [showLoading, setShowLoading] = useState(false);
const [tags, setTags] = useState<string[]>([]);
@ -92,7 +101,7 @@ const Nav = memo(
}, [data?.pages]);
const outerContainerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<any>(null);
const conversationsRef = useRef<List | null>(null);
const { moveToTop } = useNavScrolling<ConversationListResponse>({
setShowLoading,
@ -152,16 +161,18 @@ const Nav = memo(
}, [isFetchingNextPage, computedHasNextPage, fetchNextPage]);
const subHeaders = useMemo(
() => search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />,
() => (
<>
{search.enabled === null && <SearchBarSkeleton />}
{search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
</>
),
[search.enabled, isSmallScreen],
);
const headerButtons = useMemo(
() => (
<>
<Suspense fallback={null}>
<AgentMarketplaceButton isSmallScreen={isSmallScreen} toggleNav={toggleNavVisible} />
</Suspense>
{hasAccessToBookmarks && (
<>
<div className="mt-1.5" />
@ -172,7 +183,7 @@ const Nav = memo(
)}
</>
),
[hasAccessToBookmarks, tags, isSmallScreen, toggleNavVisible],
[hasAccessToBookmarks, tags],
);
const [isSearchLoading, setIsSearchLoading] = useState(
@ -210,24 +221,28 @@ const Nav = memo(
<nav
id="chat-history-nav"
aria-label={localize('com_ui_chat_history')}
className="flex h-full flex-col px-2 pb-3.5 md:px-3"
className="flex h-full flex-col px-2 pb-3.5"
>
<div className="flex flex-1 flex-col" ref={outerContainerRef}>
<div className="flex flex-1 flex-col overflow-hidden" ref={outerContainerRef}>
<MemoNewChat
subHeaders={subHeaders}
toggleNav={toggleNavVisible}
headerButtons={headerButtons}
isSmallScreen={isSmallScreen}
/>
<Conversations
conversations={conversations}
moveToTop={moveToTop}
toggleNav={itemToggleNav}
containerRef={listRef}
loadMoreConversations={loadMoreConversations}
isLoading={isFetchingNextPage || showLoading || isLoading}
isSearchLoading={isSearchLoading}
/>
<div className="flex min-h-0 flex-grow flex-col overflow-hidden">
<Conversations
conversations={conversations}
moveToTop={moveToTop}
toggleNav={itemToggleNav}
containerRef={conversationsRef}
loadMoreConversations={loadMoreConversations}
isLoading={isFetchingNextPage || showLoading || isLoading}
isSearchLoading={isSearchLoading}
isChatsExpanded={isChatsExpanded}
setIsChatsExpanded={setIsChatsExpanded}
/>
</div>
</div>
<Suspense fallback={null}>
<AccountSettings />

View file

@ -109,10 +109,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivEleme
return (
<div
ref={ref}
className={cn(
'group relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-2 border-transparent px-3 py-2 text-text-primary transition-all duration-200 focus-within:border-ring-primary focus-within:bg-surface-hover hover:bg-surface-hover',
isSmallScreen === true ? 'mb-2 h-14 rounded-xl' : '',
)}
className="group relative my-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-2 border-transparent px-3 py-2 text-text-primary focus-within:border-ring-primary focus-within:bg-surface-hover hover:bg-surface-hover"
>
<Search
aria-hidden="true"