📌 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,2 +1,3 @@
export { default as useNavScrolling } from './useNavScrolling';
export { default as useShowMarketplace } from './useShowMarketplace';
export * from './useNavHelpers';

View file

@ -0,0 +1,37 @@
import { useContext, useMemo } from 'react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useHasAccess, AuthContext } from '~/hooks';
/**
* Hook to determine if the Agent Marketplace should be shown.
* Consolidates the logic for checking:
* - Auth readiness (avoid race conditions)
* - Access to Agents permission
* - Access to Marketplace permission
*
* @returns Whether the Agent Marketplace should be displayed
*/
export default function useShowMarketplace(): boolean {
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
// Check if auth is ready (avoid race conditions)
const authReady = useMemo(
() =>
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined),
[authContext?.isAuthenticated, authContext?.user],
);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
return authReady && hasAccessToAgents && hasAccessToMarketplace;
}

View file

@ -25,6 +25,7 @@ export type { TranslationKeys } from './useLocalize';
export { default as useTimeout } from './useTimeout';
export { default as useNewConvo } from './useNewConvo';
export { default as useLocalize } from './useLocalize';
export { default as useFavorites } from './useFavorites';
export { default as useChatBadges } from './useChatBadges';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';

View file

@ -0,0 +1,206 @@
import { useEffect, useCallback, useRef } from 'react';
import { useAtom } from 'jotai';
import { useToastContext } from '@librechat/client';
import type { Favorite } from '~/store/favorites';
import { useGetFavoritesQuery, useUpdateFavoritesMutation } from '~/data-provider';
import { favoritesAtom } from '~/store';
import { useLocalize } from '~/hooks';
import { logger } from '~/utils';
/** Maximum number of favorites allowed (must match backend MAX_FAVORITES) */
const MAX_FAVORITES = 50;
/**
* Hook for managing user favorites (pinned agents and models).
*
* Favorites are synchronized with the server via `/api/user/settings/favorites`.
* Each favorite is either:
* - An agent: `{ agentId: string }`
* - A model: `{ model: string, endpoint: string }`
*
* @returns Object containing favorites state and helper methods for
* adding, removing, toggling, reordering, and checking favorites.
*/
/**
* Cleans favorites array to only include canonical shapes (agentId or model+endpoint).
*/
const cleanFavorites = (favorites: Favorite[]): Favorite[] => {
if (!Array.isArray(favorites)) {
return [];
}
return favorites.map((f) => {
if (f.agentId) {
return { agentId: f.agentId };
}
if (f.model && f.endpoint) {
return { model: f.model, endpoint: f.endpoint };
}
return f;
});
};
export default function useFavorites() {
const localize = useLocalize();
const { showToast } = useToastContext();
const [favorites, setFavorites] = useAtom(favoritesAtom);
const getFavoritesQuery = useGetFavoritesQuery();
const updateFavoritesMutation = useUpdateFavoritesMutation();
const isMutatingRef = useRef(false);
useEffect(() => {
// Skip updating local state if a mutation is in progress or just completed
// The local state is already optimistically updated by saveFavorites
if (isMutatingRef.current || updateFavoritesMutation.isLoading) {
return;
}
if (getFavoritesQuery.data) {
if (Array.isArray(getFavoritesQuery.data)) {
setFavorites(getFavoritesQuery.data);
} else {
setFavorites([]);
}
}
}, [getFavoritesQuery.data, setFavorites, updateFavoritesMutation.isLoading]);
const getErrorMessage = useCallback(
(error: unknown): string => {
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as {
response?: { data?: { code?: string; limit?: number } };
};
const { code, limit } = axiosError.response?.data ?? {};
if (code === 'MAX_FAVORITES_EXCEEDED') {
return localize('com_ui_max_favorites_reached', { 0: String(limit ?? MAX_FAVORITES) });
}
}
return localize('com_ui_error');
},
[localize],
);
const saveFavorites = useCallback(
async (newFavorites: typeof favorites) => {
const cleaned = cleanFavorites(newFavorites);
setFavorites(cleaned);
isMutatingRef.current = true;
try {
await updateFavoritesMutation.mutateAsync(cleaned);
} catch (error) {
logger.error('Error updating favorites:', error);
showToast({ message: getErrorMessage(error), status: 'error' });
// Refetch to resync state with server
getFavoritesQuery.refetch();
} finally {
// Use a small delay to prevent the useEffect from triggering immediately
// after the mutation completes but before React has finished processing
setTimeout(() => {
isMutatingRef.current = false;
}, 100);
}
},
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
);
const addFavoriteAgent = (agentId: string) => {
if (favorites.some((f) => f.agentId === agentId)) return;
const newFavorites = [...favorites, { agentId }];
saveFavorites(newFavorites);
};
const removeFavoriteAgent = (agentId: string) => {
const newFavorites = favorites.filter((f) => f.agentId !== agentId);
saveFavorites(newFavorites);
};
const addFavoriteModel = (model: { model: string; endpoint: string }) => {
if (favorites.some((f) => f.model === model.model && f.endpoint === model.endpoint)) return;
const newFavorites = [...favorites, { model: model.model, endpoint: model.endpoint }];
saveFavorites(newFavorites);
};
const removeFavoriteModel = (model: string, endpoint: string) => {
const newFavorites = favorites.filter((f) => !(f.model === model && f.endpoint === endpoint));
saveFavorites(newFavorites);
};
const isFavoriteAgent = (agentId: string | undefined | null) => {
if (!agentId) {
return false;
}
return favorites.some((f) => f.agentId === agentId);
};
const isFavoriteModel = (model: string, endpoint: string) => {
return favorites.some((f) => f.model === model && f.endpoint === endpoint);
};
const toggleFavoriteAgent = (agentId: string) => {
if (isFavoriteAgent(agentId)) {
removeFavoriteAgent(agentId);
} else {
addFavoriteAgent(agentId);
}
};
const toggleFavoriteModel = (model: { model: string; endpoint: string }) => {
if (isFavoriteModel(model.model, model.endpoint)) {
removeFavoriteModel(model.model, model.endpoint);
} else {
addFavoriteModel(model);
}
};
/**
* Reorder favorites and optionally persist the new order to the server.
* This combines state update and persistence to avoid race conditions
* where the closure captures stale state.
*/
const reorderFavorites = useCallback(
async (newFavorites: typeof favorites, persist = false) => {
const cleaned = cleanFavorites(newFavorites);
setFavorites(cleaned);
if (persist) {
isMutatingRef.current = true;
try {
await updateFavoritesMutation.mutateAsync(cleaned);
} catch (error) {
logger.error('Error reordering favorites:', error);
showToast({ message: getErrorMessage(error), status: 'error' });
// Refetch to resync state with server
getFavoritesQuery.refetch();
} finally {
setTimeout(() => {
isMutatingRef.current = false;
}, 100);
}
}
},
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
);
return {
favorites,
addFavoriteAgent,
removeFavoriteAgent,
addFavoriteModel,
removeFavoriteModel,
isFavoriteAgent,
isFavoriteModel,
toggleFavoriteAgent,
toggleFavoriteModel,
reorderFavorites,
/** Whether the favorites query is currently loading */
isLoading: getFavoritesQuery.isLoading,
/** Whether there was an error fetching favorites */
isError: getFavoritesQuery.isError,
/** Whether the update mutation is in progress */
isUpdating: updateFavoritesMutation.isLoading,
/** Error from fetching favorites, if any */
fetchError: getFavoritesQuery.error,
/** Error from updating favorites, if any */
updateError: updateFavoritesMutation.error,
};
}