🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 (#6578)

* 🔧 fix: Simplify event handling in Badge component by always preventing default behavior and stopping propagation on toggle

* feat: show Global agents icon in ModelSelector

* feat: show Global agents icon in ModelSelector's search results

* refactor(Header): remove unused import

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(EndpointModelItem): remove unused import of useGetStartupConfig

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Marco Beretta 2025-03-27 23:07:07 +01:00 committed by GitHub
parent b9ebdd4aa5
commit e630c0a00d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 76 additions and 127 deletions

View file

@ -1,11 +1,11 @@
import React from 'react';
import { TModelSpec, TInterfaceConfig } from 'librechat-data-provider';
import { TModelSpec, TStartupConfig } from 'librechat-data-provider';
export interface Endpoint {
value: string;
label: string;
hasModels: boolean;
models?: string[];
models?: Array<{ name: string; isGlobal?: boolean }>;
icon: React.ReactNode;
agentNames?: Record<string, string>;
assistantNames?: Record<string, string>;
@ -19,6 +19,6 @@ export interface SelectedValues {
}
export interface ModelSelectorProps {
interfaceConfig: TInterfaceConfig;
startupConfig: TStartupConfig | undefined;
modelSpecs: TModelSpec[];
}

View file

@ -496,17 +496,6 @@ export interface ExtendedFile {
metadata?: t.TFile['metadata'];
}
export interface ExtendedEndpoint {
value: EModelEndpoint;
label: string;
hasModels: boolean;
icon: JSX.Element | null;
models?: string[];
agentNames?: Record<string, string>;
assistantNames?: Record<string, string>;
modelIcons?: Record<string, string | undefined>;
}
export interface ModelItemProps {
modelName: string;
endpoint: EModelEndpoint;

View file

@ -9,7 +9,6 @@ import ExportAndShareMenu from './ExportAndShareMenu';
import { useMediaQuery, useHasAccess } from '~/hooks';
import BookmarkMenu from './Menus/BookmarkMenu';
import AddMultiConvo from './AddMultiConvo';
const defaultInterface = getConfigDefaults().interface;
export default function Header() {
@ -38,7 +37,7 @@ export default function Header() {
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
<div className="mx-2 flex items-center gap-2">
{!navVisible && <HeaderNewChat />}
{<ModelSelector interfaceConfig={interfaceConfig} modelSpecs={modelSpecs} />}
{<ModelSelector startupConfig={startupConfig} modelSpecs={modelSpecs} />}
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
{hasAccessToBookmarks === true && <BookmarkMenu />}
{hasAccessToMultiConvo === true && <AddMultiConvo />}

View file

@ -98,9 +98,9 @@ function ModelSelectorContent() {
);
}
export default function ModelSelector({ interfaceConfig, modelSpecs }: ModelSelectorProps) {
export default function ModelSelector({ startupConfig, modelSpecs }: ModelSelectorProps) {
return (
<ModelSelectorProvider modelSpecs={modelSpecs} interfaceConfig={interfaceConfig}>
<ModelSelectorProvider modelSpecs={modelSpecs} startupConfig={startupConfig}>
<ModelSelectorContent />
</ModelSelectorProvider>
);

View file

@ -45,13 +45,13 @@ export function useModelSelectorContext() {
interface ModelSelectorProviderProps {
children: React.ReactNode;
modelSpecs: t.TModelSpec[];
interfaceConfig: t.TInterfaceConfig;
startupConfig: t.TStartupConfig | undefined;
}
export function ModelSelectorProvider({
children,
modelSpecs,
interfaceConfig,
startupConfig,
}: ModelSelectorProviderProps) {
const agentsMap = useAgentsMapContext();
const assistantsMap = useAssistantsMapContext();
@ -61,7 +61,7 @@ export function ModelSelectorProvider({
agentsMap,
assistantsMap,
endpointsConfig,
interfaceConfig,
startupConfig,
});
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
// presets,

View file

@ -89,7 +89,13 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
if (endpoint.hasModels) {
const filteredModels = searchValue
? filterModels(endpoint, endpoint.models || [], searchValue, agentsMap, assistantsMap)
? filterModels(
endpoint,
(endpoint.models || []).map((model) => model.name),
searchValue,
agentsMap,
assistantsMap,
)
: null;
const placeholder =
isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)

View file

@ -1,4 +1,5 @@
import React from 'react';
import { EarthIcon } from 'lucide-react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
import { useModelSelectorContext } from '../ModelSelectorContext';
@ -12,12 +13,16 @@ interface EndpointModelItemProps {
export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointModelItemProps) {
const { handleSelectModel } = useModelSelectorContext();
let isGlobal = false;
let modelName = modelId;
const avatarUrl = endpoint?.modelIcons?.[modelId ?? ''] || null;
// Use custom names if available
if (endpoint && modelId && isAgentsEndpoint(endpoint.value) && endpoint.agentNames?.[modelId]) {
modelName = endpoint.agentNames[modelId];
const modelInfo = endpoint?.models?.find((m) => m.name === modelId);
isGlobal = modelInfo?.isGlobal ?? false;
} else if (
endpoint &&
modelId &&
@ -46,6 +51,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
) : null}
<span>{modelName}</span>
</div>
{isGlobal && <EarthIcon className="ml-auto size-4 text-green-400" />}
{isSelected && (
<svg
width="16"
@ -53,7 +59,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="ml-auto block"
className="block"
>
<path
fillRule="evenodd"
@ -69,11 +75,11 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
export function renderEndpointModels(
endpoint: Endpoint | null,
models: string[],
models: Array<{ name: string; isGlobal?: boolean }>,
selectedModel: string | null,
filteredModels?: string[],
) {
const modelsToRender = filteredModels || models;
const modelsToRender = filteredModels || models.map((model) => model.name);
return modelsToRender.map(
(modelId) =>

View file

@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import { EarthIcon } from 'lucide-react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
@ -20,8 +21,6 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
handleSelectModel,
handleSelectEndpoint,
endpointsConfig,
agentsMap,
assistantsMap,
} = useModelSelectorContext();
const {
@ -102,20 +101,20 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
const lowerQuery = searchValue.toLowerCase();
const filteredModels = endpoint.label.toLowerCase().includes(lowerQuery)
? endpoint.models
: endpoint.models.filter((modelId) => {
let modelName = modelId;
: endpoint.models.filter((model) => {
let modelName = model.name;
if (
isAgentsEndpoint(endpoint.value) &&
endpoint.agentNames &&
endpoint.agentNames[modelId]
endpoint.agentNames[model.name]
) {
modelName = endpoint.agentNames[modelId];
modelName = endpoint.agentNames[model.name];
} else if (
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames &&
endpoint.assistantNames[modelId]
endpoint.assistantNames[model.name]
) {
modelName = endpoint.assistantNames[modelId];
modelName = endpoint.assistantNames[model.name];
}
return modelName.toLowerCase().includes(lowerQuery);
});
@ -134,7 +133,10 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
)}
{endpoint.label}
</div>
{filteredModels.map((modelId) => {
{filteredModels.map((model) => {
const modelId = model.name;
let isGlobal = false;
let modelName = modelId;
if (
isAgentsEndpoint(endpoint.value) &&
@ -142,6 +144,8 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
endpoint.agentNames[modelId]
) {
modelName = endpoint.agentNames[modelId];
const modelInfo = endpoint?.models?.find((m) => m.name === modelId);
isGlobal = modelInfo?.isGlobal ?? false;
} else if (
isAssistantsEndpoint(endpoint.value) &&
endpoint.assistantNames &&
@ -168,6 +172,7 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
)}
<span>{modelName}</span>
</div>
{isGlobal && <EarthIcon className="ml-auto size-4 text-green-400" />}
{selectedEndpoint === endpoint.value && selectedModel === modelId && (
<svg
width="16"
@ -175,7 +180,7 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="ml-auto block"
className="block"
>
<path
fillRule="evenodd"

View file

@ -12,7 +12,12 @@ import SpecIcon from '~/components/Chat/Menus/Endpoints/components/SpecIcon';
import { Endpoint, SelectedValues } from '~/common';
export function filterItems<
T extends { label: string; name?: string; value?: string; models?: string[] },
T extends {
label: string;
name?: string;
value?: string;
models?: Array<{ name: string; isGlobal?: boolean }>;
},
>(
items: T[],
searchValue: string,
@ -36,18 +41,18 @@ export function filterItems<
if (item.models && item.models.length > 0) {
return item.models.some((modelId) => {
if (modelId.toLowerCase().includes(searchTermLower)) {
if (modelId.name.toLowerCase().includes(searchTermLower)) {
return true;
}
if (isAgentsEndpoint(item.value) && agentsMap && modelId in agentsMap) {
const agentName = agentsMap[modelId]?.name;
if (isAgentsEndpoint(item.value) && agentsMap && modelId.name in agentsMap) {
const agentName = agentsMap[modelId.name]?.name;
return typeof agentName === 'string' && agentName.toLowerCase().includes(searchTermLower);
}
if (isAssistantsEndpoint(item.value) && assistantsMap) {
const endpoint = item.value ?? '';
const assistant = assistantsMap[endpoint][modelId];
const assistant = assistantsMap[endpoint][modelId.name];
if (assistant && typeof assistant.name === 'string') {
return assistant.name.toLowerCase().includes(searchTermLower);
}

View file

@ -44,9 +44,7 @@ export default function Badge({
}
if (!isEditing && onToggle) {
if (typeof window !== 'undefined' && window.innerWidth >= 768) {
e.preventDefault();
}
e.preventDefault();
e.stopPropagation();
onToggle();
}

View file

@ -11,10 +11,10 @@ import type {
Assistant,
TEndpointsConfig,
TAgentsMap,
TInterfaceConfig,
TAssistantsMap,
TStartupConfig,
} from 'librechat-data-provider';
import type { ExtendedEndpoint } from '~/common';
import type { Endpoint } from '~/common';
import { mapEndpoints, getIconKey, getEndpointField } from '~/utils';
import { useGetEndpointsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers';
@ -25,16 +25,18 @@ export const useEndpoints = ({
agentsMap,
assistantsMap,
endpointsConfig,
interfaceConfig,
startupConfig,
}: {
agentsMap?: TAgentsMap;
assistantsMap?: TAssistantsMap;
endpointsConfig: TEndpointsConfig;
interfaceConfig: TInterfaceConfig;
startupConfig: TStartupConfig | undefined;
}) => {
const modelsQuery = useGetModelsQuery();
const { conversation } = useChatContext();
const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints });
const { instanceProjectId } = startupConfig ?? {};
const interfaceConfig = startupConfig?.interface ?? {};
const { endpoint } = conversation ?? {};
@ -84,7 +86,7 @@ export const useEndpoints = ({
[endpointsConfig],
);
const mappedEndpoints: ExtendedEndpoint[] = useMemo(() => {
const mappedEndpoints: Endpoint[] = useMemo(() => {
return filteredEndpoints.map((ep) => {
const endpointType = getEndpointField(endpointsConfig, ep, 'type');
const iconKey = getIconKey({ endpoint: ep, endpointsConfig, endpointType });
@ -98,7 +100,7 @@ export const useEndpoints = ({
(modelsQuery.data?.[ep]?.length ?? 0) > 0);
// Base result object with formatted default icon
const result: ExtendedEndpoint = {
const result: Endpoint = {
value: ep,
label: alternateName[ep] || ep,
hasModels,
@ -114,7 +116,11 @@ export const useEndpoints = ({
// Handle agents case
if (ep === EModelEndpoint.agents && agents.length > 0) {
result.models = agents.map((agent) => agent.id);
result.models = agents.map((agent) => ({
name: agent.id,
isGlobal:
(instanceProjectId != null && agent.projectIds?.includes(instanceProjectId)) ?? false,
}));
result.agentNames = agents.reduce((acc, agent) => {
acc[agent.id] = agent.name || '';
return acc;
@ -127,7 +133,10 @@ export const useEndpoints = ({
// Handle assistants case
else if (ep === EModelEndpoint.assistants && assistants.length > 0) {
result.models = assistants.map((assistant: { id: string }) => assistant.id);
result.models = assistants.map((assistant: { id: string }) => ({
name: assistant.id,
isGlobal: false,
}));
result.assistantNames = assistants.reduce(
(acc: Record<string, string>, assistant: Assistant) => {
acc[assistant.id] = assistant.name || '';
@ -143,7 +152,10 @@ export const useEndpoints = ({
{},
);
} else if (ep === EModelEndpoint.azureAssistants && azureAssistants.length > 0) {
result.models = azureAssistants.map((assistant: { id: string }) => assistant.id);
result.models = azureAssistants.map((assistant: { id: string }) => ({
name: assistant.id,
isGlobal: false,
}));
result.assistantNames = azureAssistants.reduce(
(acc: Record<string, string>, assistant: Assistant) => {
acc[assistant.id] = assistant.name || '';
@ -166,7 +178,10 @@ export const useEndpoints = ({
ep !== EModelEndpoint.assistants &&
(modelsQuery.data?.[ep]?.length ?? 0) > 0
) {
result.models = modelsQuery.data?.[ep];
result.models = modelsQuery.data?.[ep]?.map((model) => ({
name: model,
isGlobal: false,
}));
}
return result;

View file

@ -1,73 +0,0 @@
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { ExtendedEndpoint } from '~/common';
export const filterMenuItems = (
searchTerm: string,
mappedEndpoints: ExtendedEndpoint[],
agents: any[],
assistants: any[],
modelsData: any,
): ExtendedEndpoint[] => {
if (!searchTerm.trim()) {
return mappedEndpoints;
}
const lowercaseSearchTerm = searchTerm.toLowerCase();
return mappedEndpoints
.map((ep) => {
if (ep.hasModels) {
if (isAgentsEndpoint(ep.value)) {
const filteredAgents = agents.filter((agent) =>
agent.name?.toLowerCase().includes(lowercaseSearchTerm),
);
if (ep.label.toLowerCase().includes(lowercaseSearchTerm) || filteredAgents.length > 0) {
return {
...ep,
models: filteredAgents.map((agent) => agent.id),
agentNames: filteredAgents.reduce((acc: Record<string, string>, agent) => {
acc[agent.id] = agent.name || '';
return acc;
}, {}),
};
}
return null;
} else if (isAssistantsEndpoint(ep.value)) {
const filteredAssistants = assistants.filter((assistant) =>
assistant.name?.toLowerCase().includes(lowercaseSearchTerm),
);
if (
ep.label.toLowerCase().includes(lowercaseSearchTerm) ||
filteredAssistants.length > 0
) {
return {
...ep,
models: filteredAssistants.map((assistant) => assistant.id),
assistantNames: filteredAssistants.reduce(
(acc: Record<string, string>, assistant) => {
acc[assistant.id] = assistant.name || '';
return acc;
},
{},
),
};
}
return null;
} else {
const allModels = modelsData?.[ep.value] ?? [];
const filteredModels = allModels.filter((model: string) =>
model.toLowerCase().includes(lowercaseSearchTerm),
);
if (ep.label.toLowerCase().includes(lowercaseSearchTerm) || filteredModels.length > 0) {
return { ...ep, models: filteredModels };
}
return null;
}
} else {
return ep.label.toLowerCase().includes(lowercaseSearchTerm) ? { ...ep, models: [] } : null;
}
})
.filter(Boolean) as ExtendedEndpoint[];
};
export default filterMenuItems;

View file

@ -20,7 +20,6 @@ export { default as logger } from './logger';
export { default as buildTree } from './buildTree';
export { default as getLoginError } from './getLoginError';
export { default as cleanupPreset } from './cleanupPreset';
export { default as filterMenuItems } from './endpointFilter';
export { default as buildDefaultConvo } from './buildDefaultConvo';
export { default as getDefaultEndpoint } from './getDefaultEndpoint';