mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🚫 fix: Hide Empty Agents Endpoint from Model Selector (#13624)
* fix: prevent empty agents endpoint selection * fix: sort endpoint item imports
This commit is contained in:
parent
fd4728232c
commit
6fc72e5dfd
7 changed files with 143 additions and 4 deletions
|
|
@ -11,6 +11,13 @@ const agentsEndpoint: Endpoint = {
|
|||
searchAliases: ['agent marketplace', 'marketplace'],
|
||||
};
|
||||
|
||||
const disabledAgentsEndpoint: Endpoint = {
|
||||
value: 'agents',
|
||||
label: 'My Agents',
|
||||
hasModels: false,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
describe('model selector utilities', () => {
|
||||
it('matches endpoint search aliases', () => {
|
||||
const results = filterItems([agentsEndpoint], 'marketplace', undefined, undefined);
|
||||
|
|
@ -31,4 +38,9 @@ describe('model selector utilities', () => {
|
|||
const results = filterItems([agentsEndpoint], 'tienda', undefined, undefined, localize);
|
||||
expect(results).toEqual([agentsEndpoint]);
|
||||
});
|
||||
|
||||
it('does not match agents when there are no selectable agent options', () => {
|
||||
const results = filterItems([disabledAgentsEndpoint], 'my agents', undefined, undefined);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import type { TModelSpec } from 'librechat-data-provider';
|
|||
import type { Endpoint } from '~/common';
|
||||
import { CustomMenu as Menu, CustomMenuItem as MenuItem, CustomMenuSeparator } from '../CustomMenu';
|
||||
import MarketplaceItem, { marketplaceSearchMatches } from './Marketplace';
|
||||
import { filterModels, shouldRenderEndpointOption } from '../utils';
|
||||
import { useModelSelectorContext } from '../ModelSelectorContext';
|
||||
import { renderEndpointModels } from './EndpointModelItem';
|
||||
import { ModelSpecItem } from './ModelSpecItem';
|
||||
import { filterModels } from '../utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -182,6 +182,10 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
|||
|
||||
const isEndpointSelected = !selectedSpec && selectedEndpoint === endpoint.value;
|
||||
|
||||
if (!shouldRenderEndpointOption(endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (endpoint.hasModels) {
|
||||
const placeholder =
|
||||
isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { Endpoint } from '~/common';
|
|||
import MarketplaceItem, { marketplaceSearchMatches } from './Marketplace';
|
||||
import { useModelSelectorContext } from '../ModelSelectorContext';
|
||||
import { CustomMenuItem as MenuItem } from '../CustomMenu';
|
||||
import { shouldRenderEndpointOption } from '../utils';
|
||||
import SpecIcon from './SpecIcon';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -103,6 +104,10 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
|
|||
} else {
|
||||
// For an endpoint item
|
||||
const endpoint = item as Endpoint;
|
||||
if (!shouldRenderEndpointOption(endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (endpoint.hasModels) {
|
||||
const lowerQuery = searchValue.toLowerCase();
|
||||
const endpointMatches = endpoint.label.toLowerCase().includes(lowerQuery);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { Endpoint, SelectedValues } from '~/common';
|
||||
import { EndpointItem } from '../EndpointItem';
|
||||
|
||||
const mockHandleSelectEndpoint = jest.fn();
|
||||
const mockHandleOpenKeyDialog = jest.fn();
|
||||
const mockSetEndpointSearchValue = jest.fn();
|
||||
|
||||
let mockSelectedValues: SelectedValues = { endpoint: '', model: '', modelSpec: '' };
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Menus/Endpoints/ModelSelectorContext', () => ({
|
||||
useModelSelectorContext: () => ({
|
||||
agentsMap: undefined,
|
||||
assistantsMap: undefined,
|
||||
modelSpecs: [],
|
||||
selectedValues: mockSelectedValues,
|
||||
endpointSearchValues: {},
|
||||
handleOpenKeyDialog: mockHandleOpenKeyDialog,
|
||||
handleSelectEndpoint: mockHandleSelectEndpoint,
|
||||
setEndpointSearchValue: mockSetEndpointSearchValue,
|
||||
endpointRequiresUserKey: () => false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Menus/Endpoints/CustomMenu', () => {
|
||||
const React = jest.requireActual<typeof import('react')>('react');
|
||||
|
||||
return {
|
||||
CustomMenu: ({ children, label }: { children?: React.ReactNode; label?: React.ReactNode }) =>
|
||||
React.createElement('div', null, label, children),
|
||||
CustomMenuItem: React.forwardRef(function MockMenuItem(
|
||||
{ children, ...rest }: { children?: React.ReactNode },
|
||||
ref: React.Ref<HTMLButtonElement>,
|
||||
) {
|
||||
return React.createElement('button', { ref, type: 'button', ...rest }, children);
|
||||
}),
|
||||
CustomMenuSeparator: () => React.createElement('hr'),
|
||||
};
|
||||
});
|
||||
|
||||
const disabledAgentsEndpoint: Endpoint = {
|
||||
value: 'agents',
|
||||
label: 'My Agents',
|
||||
hasModels: false,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
const customEndpoint: Endpoint = {
|
||||
value: 'custom',
|
||||
label: 'Custom',
|
||||
hasModels: false,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
describe('EndpointItem', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSelectedValues = { endpoint: '', model: '', modelSpec: '' };
|
||||
});
|
||||
|
||||
it('does not render agents as a leaf endpoint when no selectable rows exist', () => {
|
||||
render(<EndpointItem endpoint={disabledAgentsEndpoint} endpointIndex={0} />);
|
||||
|
||||
expect(screen.queryByText('My Agents')).not.toBeInTheDocument();
|
||||
expect(mockHandleSelectEndpoint).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps non-agent endpoints without models selectable', () => {
|
||||
render(<EndpointItem endpoint={customEndpoint} endpointIndex={0} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Custom' }));
|
||||
|
||||
expect(mockHandleSelectEndpoint).toHaveBeenCalledWith(customEndpoint);
|
||||
});
|
||||
});
|
||||
|
|
@ -70,6 +70,13 @@ const agentsMarketplaceEndpoint: Endpoint = {
|
|||
icon: null,
|
||||
};
|
||||
|
||||
const disabledAgentsEndpoint: Endpoint = {
|
||||
value: 'agents',
|
||||
label: 'My Agents',
|
||||
hasModels: false,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
describe('SearchResults', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -140,4 +147,18 @@ describe('SearchResults', () => {
|
|||
expect(mockNavigate).toHaveBeenCalledWith('/agents');
|
||||
expect(mockHandleSelectModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render agents as a selectable endpoint when marketplace and agent rows are unavailable', () => {
|
||||
mockSelectedValues = { endpoint: '', model: '', modelSpec: '' };
|
||||
render(
|
||||
<SearchResults
|
||||
results={[disabledAgentsEndpoint]}
|
||||
localize={localize}
|
||||
searchValue="my agents"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('menuitem', { name: 'My Agents' })).not.toBeInTheDocument();
|
||||
expect(mockHandleSelectEndpoint).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function filterItems<
|
|||
label: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
hasModels?: boolean;
|
||||
models?: Array<{ name: string; isGlobal?: boolean }>;
|
||||
searchAliases?: string[];
|
||||
showMarketplace?: boolean;
|
||||
|
|
@ -33,6 +34,10 @@ export function filterItems<
|
|||
}
|
||||
|
||||
return items.filter((item) => {
|
||||
if (!shouldRenderEndpointOption(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemMatches =
|
||||
item.label.toLowerCase().includes(searchTermLower) ||
|
||||
(item.name && item.name.toLowerCase().includes(searchTermLower)) ||
|
||||
|
|
@ -76,6 +81,13 @@ export function filterItems<
|
|||
});
|
||||
}
|
||||
|
||||
export function shouldRenderEndpointOption(endpoint: {
|
||||
value?: string;
|
||||
hasModels?: boolean;
|
||||
}): boolean {
|
||||
return !isAgentsEndpoint(endpoint.value) || endpoint.hasModels === true;
|
||||
}
|
||||
|
||||
export function filterModels(
|
||||
endpoint: Endpoint,
|
||||
models: string[],
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const useEndpoints = ({
|
|||
);
|
||||
|
||||
const mappedEndpoints: Endpoint[] = useMemo(() => {
|
||||
return filteredEndpoints.map((ep) => {
|
||||
return filteredEndpoints.reduce<Endpoint[]>((acc, ep) => {
|
||||
const endpointType = getEndpointField(endpointsConfig, ep, 'type');
|
||||
const iconKey = getIconKey({ endpoint: ep, endpointsConfig, endpointType });
|
||||
const Icon = icons[iconKey];
|
||||
|
|
@ -96,6 +96,10 @@ export const useEndpoints = ({
|
|||
ep !== EModelEndpoint.agents &&
|
||||
(modelsQuery.data?.[ep]?.length ?? 0) > 0);
|
||||
|
||||
if (ep === EModelEndpoint.agents && !hasModels) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Base result object with formatted default icon
|
||||
const result: Endpoint = {
|
||||
value: ep,
|
||||
|
|
@ -185,8 +189,9 @@ export const useEndpoints = ({
|
|||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
acc.push(result);
|
||||
return acc;
|
||||
}, []);
|
||||
}, [
|
||||
agents,
|
||||
assistants,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue