diff --git a/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx b/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx index eb1081435d..3c4cb840a4 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx @@ -1,6 +1,7 @@ import React, { memo, useState } from 'react'; import { AlertCircle } from 'lucide-react'; import type { IconMapProps } from '~/common'; +import { getKnownEndpointAsset, hasKnownEndpointIcon } from '~/hooks/Endpoint/UnknownIcon'; import { icons } from '~/hooks/Endpoint/Icons'; interface GroupIconProps { @@ -42,13 +43,27 @@ const GroupIcon: React.FC = ({ iconURL, groupName }) => { ); } + const resolvedIconURL = getKnownEndpointAsset(iconURL); + + if (!resolvedIconURL && hasKnownEndpointIcon(iconURL)) { + const Icon: IconType = icons.unknown as IconType; + return ( + + ); + } + return (
{groupName} { + const React = jest.requireActual('react'); + const createIcon = + (iconKey: string) => + ({ className, endpoint }: { className?: string; endpoint?: string | null }) => + React.createElement('span', { + className, + 'data-testid': 'endpoint-icon', + 'data-icon-key': iconKey, + 'data-endpoint': endpoint ?? '', + }); + + return { + icons: { + openAI: createIcon('openAI'), + unknown: createIcon('unknown'), + }, + }; +}); + +describe('GroupIcon', () => { + it('renders built-in endpoint icon keys', () => { + render(); + + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-icon-key', 'openAI'); + }); + + it('resolves known endpoint asset aliases case-insensitively', () => { + render(); + + expect(screen.getByRole('img', { name: 'OpenRouter' })).toHaveAttribute( + 'src', + 'assets/openrouter.png', + ); + }); + + it('resolves known endpoint asset aliases to shipped file paths', () => { + render(); + + expect(screen.getByRole('img', { name: 'Helicone' })).toHaveAttribute( + 'src', + 'assets/helicone.svg', + ); + }); + + it('renders known endpoint aliases backed by components', () => { + render(); + + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-icon-key', 'unknown'); + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-endpoint', 'Moonshot'); + }); + + it('renders configured image URLs directly', () => { + render(); + + expect(screen.getByRole('img', { name: 'OpenRouter' })).toHaveAttribute( + 'src', + '/assets/openrouter.png', + ); + }); +}); diff --git a/client/src/hooks/Endpoint/UnknownIcon.tsx b/client/src/hooks/Endpoint/UnknownIcon.tsx index be7531a34a..e7b245a797 100644 --- a/client/src/hooks/Endpoint/UnknownIcon.tsx +++ b/client/src/hooks/Endpoint/UnknownIcon.tsx @@ -4,28 +4,49 @@ import { CustomMinimalIcon, XAIcon, MoonshotIcon } from '@librechat/client'; import { IconContext } from '~/common'; import { cn } from '~/utils'; -const knownEndpointAssets = { +const knownEndpointAssets: Record = { [KnownEndpoints.anyscale]: 'assets/anyscale.png', [KnownEndpoints.apipie]: 'assets/apipie.png', [KnownEndpoints.cohere]: 'assets/cohere.png', [KnownEndpoints.deepseek]: 'assets/deepseek.svg', [KnownEndpoints.fireworks]: 'assets/fireworks.png', - [KnownEndpoints.google]: 'assets/google.svg', + google: 'assets/google.svg', [KnownEndpoints.groq]: 'assets/groq.png', - [KnownEndpoints.helicone]: 'assets/helicone.png', + [KnownEndpoints.helicone]: 'assets/helicone.svg', [KnownEndpoints.huggingface]: 'assets/huggingface.svg', [KnownEndpoints.mistral]: 'assets/mistral.png', [KnownEndpoints.mlx]: 'assets/mlx.png', [KnownEndpoints.ollama]: 'assets/ollama.png', - [KnownEndpoints.openai]: 'assets/openai.svg', + openai: 'assets/openai.svg', [KnownEndpoints.openrouter]: 'assets/openrouter.png', [KnownEndpoints.perplexity]: 'assets/perplexity.png', - [KnownEndpoints.qwen]: 'assets/qwen.svg', + qwen: 'assets/qwen.svg', [KnownEndpoints.shuttleai]: 'assets/shuttleai.png', [KnownEndpoints['together.ai']]: 'assets/together.png', [KnownEndpoints.unify]: 'assets/unify.webp', }; +const knownEndpointComponents = new Set([KnownEndpoints.moonshot, KnownEndpoints.xai]); + +export function getKnownEndpointAsset(endpoint?: string | null): string { + if (!endpoint) { + return ''; + } + + return knownEndpointAssets[endpoint.toLowerCase()] ?? ''; +} + +export function hasKnownEndpointIcon(endpoint?: string | null): boolean { + if (!endpoint) { + return false; + } + + const currentEndpoint = endpoint.toLowerCase(); + return ( + getKnownEndpointAsset(currentEndpoint) !== '' || knownEndpointComponents.has(currentEndpoint) + ); +} + const knownEndpointClasses = { [KnownEndpoints.cohere]: { [IconContext.landing]: 'p-2', @@ -81,7 +102,7 @@ function UnknownIcon({ return {`${endpoint}; } - const assetPath: string = knownEndpointAssets[currentEndpoint] ?? ''; + const assetPath = getKnownEndpointAsset(currentEndpoint); if (!assetPath) { return ;