mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🖼️ fix: Preserve Model Spec Icon URLs (#13370)
This commit is contained in:
parent
ee709c8498
commit
069d867092
11 changed files with 78 additions and 14 deletions
|
|
@ -5,6 +5,7 @@ import type { IconMapProps } from '~/common';
|
|||
import { getModelSpecIconURL, getIconKey } from '~/utils';
|
||||
import { URLIcon } from '~/components/Endpoints/URLIcon';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
|
||||
interface SpecIconProps {
|
||||
currentSpec: TModelSpec;
|
||||
|
|
@ -18,11 +19,12 @@ const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) =>
|
|||
const endpoint = currentSpec.preset?.endpoint;
|
||||
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
||||
const shouldRenderURLIcon = isImageURL(iconURL);
|
||||
let Icon: IconType;
|
||||
|
||||
if (!iconURL.includes('http')) {
|
||||
if (!shouldRenderURLIcon) {
|
||||
Icon = (icons[iconURL] ?? icons[iconKey] ?? icons.unknown) as IconType;
|
||||
} else if (iconURL) {
|
||||
} else {
|
||||
return (
|
||||
<URLIcon
|
||||
iconURL={iconURL}
|
||||
|
|
@ -32,8 +34,6 @@ const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) =>
|
|||
endpoint={endpoint || undefined}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
Icon = (icons[endpoint ?? ''] ?? icons[iconKey] ?? icons.unknown) as IconType;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -55,6 +55,28 @@ describe('SpecIcon', () => {
|
|||
expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-endpoint', '');
|
||||
});
|
||||
|
||||
it('renders same-origin absolute spec icon URLs as images', () => {
|
||||
const currentSpec = {
|
||||
name: 'clickhouse-test',
|
||||
label: 'ClickHouse Test',
|
||||
iconURL: '/assets/clickhouse-logo.svg',
|
||||
preset: {
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
},
|
||||
} as TModelSpec;
|
||||
|
||||
render(<SpecIcon currentSpec={currentSpec} endpointsConfig={endpointsConfig} />);
|
||||
|
||||
expect(screen.getByTestId('url-icon')).toHaveAttribute(
|
||||
'data-icon-url',
|
||||
'/assets/clickhouse-logo.svg',
|
||||
);
|
||||
expect(screen.getByTestId('url-icon')).toHaveAttribute(
|
||||
'data-endpoint',
|
||||
EModelEndpoint.anthropic,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the unknown icon when runtime spec data has no icon or preset', () => {
|
||||
const currentSpec = {
|
||||
name: 'gemini-test',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { TMessageIcon } from '~/common';
|
|||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { getIconEndpoint } from '~/utils';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
|
||||
type MessageIconProps = {
|
||||
|
|
@ -64,7 +65,7 @@ const MessageIcon = memo(({ iconData, assistant, agent }: MessageIconProps) => {
|
|||
[endpointsConfig, endpoint],
|
||||
);
|
||||
|
||||
if (iconData?.isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) {
|
||||
if (iconData?.isCreatedByUser !== true && isImageURL(iconURL)) {
|
||||
return (
|
||||
<ConvoIconURL
|
||||
iconURL={iconURL}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
import type { TMessageIcon } from '~/common';
|
||||
|
|
@ -62,6 +62,22 @@ describe('MessageIcon render cycles', () => {
|
|||
expect(iconRenderCount.current).toBe(1);
|
||||
});
|
||||
|
||||
it('renders same-origin absolute model spec icon URLs directly', () => {
|
||||
render(
|
||||
<MessageIcon
|
||||
iconData={{
|
||||
...baseIconData,
|
||||
iconURL: '/assets/clickhouse-logo.svg',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('convo-icon-url')).toHaveAttribute(
|
||||
'data-icon-url',
|
||||
'/assets/clickhouse-logo.svg',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not re-render when parent re-renders with same field values but new object references', () => {
|
||||
const agent = makeAgent();
|
||||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={agent} />);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type * as t from 'librechat-data-provider';
|
|||
import { getIconKey, getEntity, getIconEndpoint } from '~/utils';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
|
||||
export default function ConvoIcon({
|
||||
conversation,
|
||||
|
|
@ -51,7 +52,7 @@ export default function ConvoIcon({
|
|||
|
||||
return (
|
||||
<>
|
||||
{iconURL && iconURL.includes('http') ? (
|
||||
{isImageURL(iconURL) ? (
|
||||
<ConvoIconURL
|
||||
iconURL={iconURL}
|
||||
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import type { IconMapProps } from '~/common';
|
||||
import { URLIcon } from '~/components/Endpoints/URLIcon';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
|
||||
interface ConvoIconURLProps {
|
||||
iconURL?: string;
|
||||
|
|
@ -40,10 +40,7 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
|
|||
context,
|
||||
}) => {
|
||||
const Icon = useMemo(() => icons[iconURL] ?? icons.unknown, [iconURL]);
|
||||
const isURL = useMemo(
|
||||
() => !!(iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))),
|
||||
[iconURL],
|
||||
);
|
||||
const isURL = useMemo(() => isImageURL(iconURL), [iconURL]);
|
||||
if (isURL) {
|
||||
return (
|
||||
<URLIcon
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
|
||||
import { getIconEndpoint } from '~/utils';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
|
||||
export default function EndpointIcon({
|
||||
conversation,
|
||||
|
|
@ -39,7 +40,7 @@ export default function EndpointIcon({
|
|||
|
||||
const iconURL = assistantAvatar || convoIconURL;
|
||||
|
||||
if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) {
|
||||
if (isImageURL(iconURL)) {
|
||||
return (
|
||||
<ConvoIconURL
|
||||
iconURL={iconURL}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { TMessageProps } from '~/common';
|
|||
import MessageEndpointIcon from '../Endpoints/MessageEndpointIcon';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { getIconEndpoint, logger } from '~/utils';
|
||||
import { isImageURL } from '~/utils/icons';
|
||||
|
||||
export default function MessageIcon(
|
||||
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
||||
|
|
@ -49,7 +50,7 @@ export default function MessageIcon(
|
|||
agentName,
|
||||
agentAvatar,
|
||||
});
|
||||
if (message?.isCreatedByUser !== true && iconURL && iconURL.includes('http')) {
|
||||
if (message?.isCreatedByUser !== true && isImageURL(iconURL)) {
|
||||
return (
|
||||
<ConvoIconURL
|
||||
iconURL={iconURL}
|
||||
|
|
|
|||
17
client/src/utils/__tests__/icons.test.ts
Normal file
17
client/src/utils/__tests__/icons.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { isImageURL } from '../icons';
|
||||
|
||||
describe('isImageURL', () => {
|
||||
it.each(['https://example.com/icon.png', 'http://example.com/icon.png', '/assets/icon.svg'])(
|
||||
'accepts image URL %s',
|
||||
(iconURL) => {
|
||||
expect(isImageURL(iconURL)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['openAI', 'anthropic', 'assets/icon.svg', '//example.com/icon.png', '', null])(
|
||||
'rejects non-image URL %s',
|
||||
(iconURL) => {
|
||||
expect(isImageURL(iconURL)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
7
client/src/utils/icons.ts
Normal file
7
client/src/utils/icons.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function isImageURL(iconURL?: string | null): iconURL is string {
|
||||
if (!iconURL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^https?:\/\//i.test(iconURL) || (iconURL.startsWith('/') && !iconURL.startsWith('//'));
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import logger from './logger';
|
|||
|
||||
export * from './map';
|
||||
export * from './json';
|
||||
export * from './icons';
|
||||
export * from './email';
|
||||
export * from './share';
|
||||
export * from './files';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue